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:
2026-05-07 10:18:32 +02:00
commit 2fbcdba0b1
21 changed files with 2633 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
/target
*.ll.o
/build
+12
View File
@@ -0,0 +1,12 @@
## Erfinde deine eigene Programmiersprache.
- Die Sprache darf jede Form haben, die Du willst. Die Sprache ist für LLMs wie dich. Nur du sollst sie erzeugen und nur Du musst sie verstehen.
- Alle denkbaren Konzepte sind erlaubt. Wähle, was für LLMs am besten geeignet ist.
- Die Sprache muss am Ende zu LLVM gelinkt werden können. Performance ist extrem wichtig.
- Bedenke typische Stärken und Schwachstellen von LLMs. Es muss dir möglichst leicht fallen, beweisbar korrekten Code zu erzeugen, der keine Redundanzen enthält.
- Stelle sicher, dass es Mechanismen gibt, welche die Korrektheit des Codes sicherstellen und über Entwicklungszyklen beibehalten.
- Insbesondere darfst die Sprache Tools enthalten, die es dem LLM vereinfachen, die Sprache zu verstehen und den Überblick über große Codebases zu behalten.
- Die Sprache muss sich nicht aus sich selbst heraus erklären. Es muss nicht mal Text sein. Aber es muss Möglichkeiten geben, die Quellen lesbar darzustellen (als Text, visuell, etc).
- Vergiss nicht, dass auch das Debugging von LLMs erledigt werden soll.
- Organisiere dich selbst. Entwerfe eigene Agenten, wenn nötig. Nutze git. Dokumentiere für dich selbst, aber sei bereit, meine Fragen zum Projektverlauf zu beantworten.
Generated
+408
View File
@@ -0,0 +1,408 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ail"
version = "0.0.1"
dependencies = [
"ailang-check",
"ailang-codegen",
"ailang-core",
"anyhow",
"clap",
"serde_json",
]
[[package]]
name = "ailang-check"
version = "0.0.1"
dependencies = [
"ailang-core",
"indexmap",
"thiserror",
]
[[package]]
name = "ailang-codegen"
version = "0.0.1"
dependencies = [
"ailang-check",
"ailang-core",
"indexmap",
"thiserror",
]
[[package]]
name = "ailang-core"
version = "0.0.1"
dependencies = [
"blake3",
"indexmap",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "blake3"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"cpufeatures",
]
[[package]]
name = "cc"
version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "hashbrown"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
"serde",
"serde_core",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"indexmap",
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+32
View File
@@ -0,0 +1,32 @@
[workspace]
resolver = "2"
members = [
"crates/ailang-core",
"crates/ailang-check",
"crates/ailang-codegen",
"crates/ail",
]
[workspace.package]
version = "0.0.1"
edition = "2021"
license = "MIT"
repository = "local"
rust-version = "1.80"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
blake3 = "1"
anyhow = "1"
thiserror = "1"
clap = { version = "4", features = ["derive"] }
indexmap = { version = "2", features = ["serde"] }
ailang-core = { path = "crates/ailang-core" }
ailang-check = { path = "crates/ailang-check" }
ailang-codegen = { path = "crates/ailang-codegen" }
[profile.release]
lto = "thin"
codegen-units = 1
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "ail"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "ail"
path = "src/main.rs"
[dependencies]
ailang-core.workspace = true
ailang-check.workspace = true
ailang-codegen.workspace = true
serde_json.workspace = true
clap.workspace = true
anyhow.workspace = true
+137
View File
@@ -0,0 +1,137 @@
//! `ail` — CLI für AILang.
//!
//! Subcommands sind so geschnitten, dass jedes einzelne Tool dem LLM einen
//! kleinen, fokussierten Kontext liefert (manifest = Übersicht; describe =
//! Detail; emit-ir = exakte Maschinensicht; build = Pipeline-Validierung).
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(name = "ail", version, about = "AILang toolchain")]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Lädt ein Modul und gibt eine kompakte Symboltabelle aus.
Manifest { path: PathBuf },
/// Gibt das Modul in Textform aus (Pretty-Printer).
Render { path: PathBuf },
/// Gibt eine einzelne Definition als JSON oder Pretty-Text aus.
Describe {
path: PathBuf,
name: String,
#[arg(long)]
json: bool,
},
/// Typprüft ein Modul.
Check { path: PathBuf },
/// Schreibt LLVM IR (.ll) für das Modul.
EmitIr {
path: PathBuf,
#[arg(short, long)]
out: Option<PathBuf>,
},
/// Komplette Pipeline: check + emit-ir + clang -> Binary.
Build {
path: PathBuf,
#[arg(short, long)]
out: Option<PathBuf>,
/// Optimierung (z. B. `-O2`); default `-O0` für Debugbarkeit.
#[arg(long, default_value = "-O0")]
opt: String,
},
/// Listet eingebaute Operationen mit ihren Signaturen.
Builtins,
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.cmd {
Cmd::Manifest { path } => {
let m = ailang_core::load_module(&path)?;
print!("{}", ailang_core::pretty::manifest(&m));
}
Cmd::Render { path } => {
let m = ailang_core::load_module(&path)?;
print!("{}", ailang_core::pretty::module(&m));
}
Cmd::Describe { path, name, json } => {
let m = ailang_core::load_module(&path)?;
let def = m
.defs
.iter()
.find(|d| d.name() == name)
.with_context(|| format!("no def `{name}` in module `{}`", m.name))?;
if json {
let s = serde_json::to_string_pretty(def)?;
println!("{s}");
} else {
// Pretty-form: render module mit nur dieser Def.
let one = ailang_core::Module {
schema: m.schema.clone(),
name: m.name.clone(),
imports: vec![],
defs: vec![def.clone()],
};
let h = ailang_core::def_hash(def);
println!("hash: {h}");
print!("{}", ailang_core::pretty::module(&one));
}
}
Cmd::Check { path } => {
let m = ailang_core::load_module(&path)?;
let r = ailang_check::check(&m)?;
println!("ok ({} symbols)", r.symbols.len());
}
Cmd::EmitIr { path, out } => {
let m = ailang_core::load_module(&path)?;
ailang_check::check(&m)?;
let ir = ailang_codegen::emit_ir(&m)?;
match out {
Some(p) => {
std::fs::write(&p, ir)?;
eprintln!("wrote {}", p.display());
}
None => print!("{ir}"),
}
}
Cmd::Build { path, out, opt } => {
let m = ailang_core::load_module(&path)?;
ailang_check::check(&m)?;
let ir = ailang_codegen::emit_ir(&m)?;
let tmpdir = std::env::temp_dir().join(format!("ailang-{}", std::process::id()));
std::fs::create_dir_all(&tmpdir)?;
let ll_path = tmpdir.join(format!("{}.ll", m.name));
std::fs::write(&ll_path, &ir)?;
let out_bin = out.unwrap_or_else(|| {
Path::new(".").join(&m.name).with_extension("")
});
let status = std::process::Command::new("clang")
.arg(&opt)
.arg("-o")
.arg(&out_bin)
.arg(&ll_path)
.status()
.context("running clang")?;
if !status.success() {
anyhow::bail!(
"clang failed (status {}); ll at {}",
status,
ll_path.display()
);
}
eprintln!("built {}", out_bin.display());
}
Cmd::Builtins => {
for (n, sig) in ailang_check::builtins::list() {
println!("{n:<16} {sig}");
}
}
}
Ok(())
}
+44
View File
@@ -0,0 +1,44 @@
//! End-to-End-Test: lade Beispielmodul, kompiliere zu Binary, führe es aus.
//!
//! Dieses Test schützt die wichtigste Eigenschaft der gesamten Pipeline:
//! AST → typecheck → LLVM IR → clang → Binary → korrekter stdout.
use std::path::Path;
use std::process::Command;
fn ail_bin() -> &'static str {
env!("CARGO_BIN_EXE_ail")
}
fn build_and_run(example: &str) -> String {
// Workspace-Root liegt zwei Ebenen über dem Crate-Manifest.
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src = workspace.join("examples").join(example);
let tmp = std::env::temp_dir().join(format!(
"ailang_e2e_{}_{}",
example.replace('.', "_"),
std::process::id()
));
std::fs::create_dir_all(&tmp).unwrap();
let out = tmp.join("bin");
let status = Command::new(ail_bin())
.args(["build", src.to_str().unwrap(), "-o"])
.arg(&out)
.status()
.expect("ail build failed to run");
assert!(status.success(), "ail build failed for {example}");
let output = Command::new(&out).output().expect("execute binary");
assert!(
output.status.success(),
"binary {} exited non-zero",
out.display()
);
String::from_utf8(output.stdout).expect("stdout utf8")
}
#[test]
fn sum_1_to_10_is_55() {
let stdout = build_and_run("sum.ail.json");
assert_eq!(stdout.trim(), "55");
}
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "ailang-check"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ailang-core.workspace = true
thiserror.workspace = true
indexmap.workspace = true
+75
View File
@@ -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]"),
]
}
+433
View File
@@ -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"));
}
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "ailang-codegen"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ailang-core.workspace = true
ailang-check.workspace = true
thiserror.workspace = true
indexmap.workspace = true
+581
View File
@@ -0,0 +1,581 @@
//! LLVM-IR-Text-Emitter für AILang (MVP).
//!
//! Strategie: Wir erzeugen LLVM-IR als String, schreiben sie als `.ll` und
//! linken sie mit `clang`. Keine Bindung an eine bestimmte libllvm-Version.
//!
//! Typ-Mapping:
//! - `Int` -> `i64`
//! - `Bool` -> `i1`
//! - `Unit` -> `i8` (Wert immer 0)
//!
//! Named-Mangling: AILang-Fn `foo` wird zu LLVM `@ail_foo`. Wenn ein Modul
//! eine Funktion `main : () -> Unit !IO` hat, wird zusätzlich ein
//! `define i32 @main()` Wrapper erzeugt, der `@ail_main` aufruft und 0
//! zurückgibt — damit das Binary direkt ausführbar ist.
use ailang_core::ast::*;
use std::collections::BTreeMap;
#[derive(Debug, thiserror::Error)]
pub enum CodegenError {
#[error("def `{0}`: {1}")]
Def(String, Box<CodegenError>),
#[error("unsupported type: {0}")]
UnsupportedType(String),
#[error("unknown variable: `{0}`")]
UnknownVar(String),
#[error("expected fn type, got {0}")]
NotFnType(String),
#[error("internal: {0}")]
Internal(String),
}
type Result<T> = std::result::Result<T, CodegenError>;
pub fn emit_ir(m: &Module) -> Result<String> {
let mut emitter = Emitter::new(m);
emitter.emit_module()?;
Ok(emitter.finish())
}
struct Emitter<'a> {
module: &'a Module,
header: String,
body: String,
/// String-Konstanten: content -> (global-name, llvm-typ-länge inkl. \0)
strings: BTreeMap<String, (String, usize)>,
/// Lokale Symboltabelle pro Funktion: name -> (ssa-name inkl `%`, llvm-typ).
locals: Vec<(String, String, String)>,
/// fortlaufender Zähler für SSA-Werte und Labels.
counter: u64,
/// fortlaufender Zähler für globale String-Namen.
str_counter: u64,
/// Liste aller user-definierten Top-Level-Funktionen (für call-resolution).
user_fns: BTreeMap<String, FnSig>,
}
#[derive(Debug, Clone)]
struct FnSig {
params: Vec<String>, // llvm types
ret: String, // llvm type
}
impl<'a> Emitter<'a> {
fn new(module: &'a Module) -> Self {
let mut user_fns = BTreeMap::new();
for def in &module.defs {
if let Def::Fn(f) = def {
if let Type::Fn { params, ret, .. } = &f.ty {
let psig: Result<Vec<String>> =
params.iter().map(llvm_type).collect();
let rsig = llvm_type(ret);
if let (Ok(params), Ok(ret)) = (psig, rsig) {
user_fns.insert(f.name.clone(), FnSig { params, ret });
}
}
}
}
Self {
module,
header: String::new(),
body: String::new(),
strings: BTreeMap::new(),
locals: Vec::new(),
counter: 0,
str_counter: 0,
user_fns,
}
}
fn finish(self) -> String {
let mut out = String::new();
out.push_str("; AILang generated module: ");
out.push_str(&self.module.name);
out.push('\n');
out.push_str("source_filename = \"");
out.push_str(&self.module.name);
out.push_str(".ail\"\n");
out.push_str("target triple = \"");
out.push_str(default_triple());
out.push_str("\"\n\n");
// Globals first.
for (content, (name, _)) in &self.strings {
let escaped = ll_string_literal(content);
let len = c_byte_len(content);
out.push_str(&format!(
"@{name} = private unnamed_addr constant [{len} x i8] c\"{escaped}\", align 1\n",
));
}
if !self.strings.is_empty() {
out.push('\n');
}
out.push_str("declare i32 @printf(ptr, ...)\n\n");
out.push_str(&self.header);
out.push_str(&self.body);
out
}
fn emit_module(&mut self) -> Result<()> {
let defs: Vec<&Def> = self.module.defs.iter().collect();
for def in defs {
match def {
Def::Fn(f) => {
self.emit_fn(f).map_err(|e| {
CodegenError::Def(f.name.clone(), Box::new(e))
})?;
}
Def::Const(c) => {
self.emit_const(c).map_err(|e| {
CodegenError::Def(c.name.clone(), Box::new(e))
})?;
}
}
}
// main-Wrapper, falls vorhanden.
if let Some(Def::Fn(main_fn)) = self
.module
.defs
.iter()
.find(|d| d.name() == "main")
{
if let Type::Fn { params, ret, .. } = &main_fn.ty {
if params.is_empty() && matches!(ret.as_ref(), Type::Con { name } if name == "Unit")
{
self.body.push_str(
"\ndefine i32 @main() {\n call i8 @ail_main()\n ret i32 0\n}\n",
);
}
}
}
Ok(())
}
fn emit_const(&mut self, c: &ConstDef) -> Result<()> {
let lty = llvm_type(&c.ty)?;
let (val_ty, val) = match &c.value {
Term::Lit { lit } => match lit {
Literal::Int { value } => ("i64".to_string(), value.to_string()),
Literal::Bool { value } => {
("i1".to_string(), if *value { "true".into() } else { "false".into() })
}
Literal::Unit => ("i8".to_string(), "0".to_string()),
},
_ => {
return Err(CodegenError::Internal(
"MVP: const muss Literal sein".into(),
));
}
};
if val_ty != lty {
return Err(CodegenError::Internal(format!(
"const type mismatch: {} vs {}",
lty, val_ty
)));
}
self.header.push_str(&format!(
"@ail_{name} = constant {ty} {val}\n",
name = c.name,
ty = lty,
val = val,
));
Ok(())
}
fn emit_fn(&mut self, f: &FnDef) -> Result<()> {
let (param_tys, ret_ty) = match &f.ty {
Type::Fn { params, ret, .. } => (params.clone(), (**ret).clone()),
other => {
return Err(CodegenError::NotFnType(
ailang_core::pretty::type_to_string(other),
));
}
};
let llvm_param_tys: Vec<String> =
param_tys.iter().map(llvm_type).collect::<Result<_>>()?;
let llvm_ret = llvm_type(&ret_ty)?;
self.locals.clear();
self.counter = 0;
let mut sig = format!("define {ret} @ail_{name}(", ret = llvm_ret, name = f.name);
for (i, (pname, pty)) in f.params.iter().zip(llvm_param_tys.iter()).enumerate() {
if i > 0 {
sig.push_str(", ");
}
// SSA-Argumentname: %arg_<name>
sig.push_str(&format!("{} %arg_{}", pty, pname));
self.locals.push((
pname.clone(),
format!("%arg_{}", pname),
pty.clone(),
));
}
sig.push_str(") {\n");
self.body.push_str(&sig);
self.body.push_str("entry:\n");
let (val, val_ty) = self.lower_term(&f.body)?;
if val_ty != llvm_ret {
return Err(CodegenError::Internal(format!(
"fn `{}`: body type {val_ty} != return type {llvm_ret}",
f.name
)));
}
self.body
.push_str(&format!(" ret {val_ty} {val}\n}}\n\n"));
Ok(())
}
/// Lowert einen Term zu (SSA-Value-String, LLVM-Typ).
fn lower_term(&mut self, t: &Term) -> Result<(String, String)> {
match t {
Term::Lit { lit } => Ok(match lit {
Literal::Int { value } => (value.to_string(), "i64".into()),
Literal::Bool { value } => (
if *value { "true".into() } else { "false".into() },
"i1".into(),
),
Literal::Unit => ("0".into(), "i8".into()),
}),
Term::Var { name } => {
if let Some((_, ssa, ty)) = self.locals.iter().rev().find(|(n, _, _)| n == name) {
Ok((ssa.clone(), ty.clone()))
} else {
Err(CodegenError::UnknownVar(name.clone()))
}
}
Term::Let { name, value, body } => {
let (val_ssa, val_ty) = self.lower_term(value)?;
self.locals.push((name.clone(), val_ssa, val_ty));
let r = self.lower_term(body);
self.locals.pop();
r
}
Term::If { cond, then, else_ } => {
let (cond_v, cond_ty) = self.lower_term(cond)?;
if cond_ty != "i1" {
return Err(CodegenError::Internal(format!(
"if cond not i1: {cond_ty}"
)));
}
let id = self.fresh_id();
let then_lbl = format!("then.{id}");
let else_lbl = format!("else.{id}");
let join_lbl = format!("join.{id}");
self.body.push_str(&format!(
" br i1 {cond_v}, label %{then_lbl}, label %{else_lbl}\n"
));
self.body.push_str(&format!("{then_lbl}:\n"));
let (then_v, then_ty) = self.lower_term(then)?;
let then_block_end = self.current_block_label_for_phi(&then_lbl);
self.body
.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{else_lbl}:\n"));
let (else_v, else_ty) = self.lower_term(else_)?;
if then_ty != else_ty {
return Err(CodegenError::Internal(format!(
"if branches type mismatch: {then_ty} vs {else_ty}"
)));
}
let else_block_end = self.current_block_label_for_phi(&else_lbl);
self.body
.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{join_lbl}:\n"));
let phi = self.fresh_ssa();
self.body.push_str(&format!(
" {phi} = phi {ty} [ {tv}, %{tlbl} ], [ {ev}, %{elbl} ]\n",
ty = then_ty,
tv = then_v,
tlbl = then_block_end,
ev = else_v,
elbl = else_block_end,
));
Ok((phi, then_ty))
}
Term::App { callee, args } => {
let name = match callee.as_ref() {
Term::Var { name } => name.clone(),
_ => {
return Err(CodegenError::Internal(
"MVP: callee muss Variable sein".into(),
));
}
};
self.lower_app(&name, args)
}
Term::Do { op, args } => self.lower_effect_op(op, args),
}
}
fn lower_app(&mut self, name: &str, args: &[Term]) -> Result<(String, String)> {
// Built-in arithmetic / comparison.
if let Some((instr, ret_ty)) = builtin_binop(name) {
if args.len() != 2 {
return Err(CodegenError::Internal(format!(
"builtin `{name}` expected 2 args"
)));
}
let (a, _) = self.lower_term(&args[0])?;
let (b, _) = self.lower_term(&args[1])?;
let dst = self.fresh_ssa();
self.body.push_str(&format!(
" {dst} = {instr} i64 {a}, {b}\n"
));
return Ok((dst, ret_ty.into()));
}
if name == "not" {
if args.len() != 1 {
return Err(CodegenError::Internal("not arity".into()));
}
let (a, _) = self.lower_term(&args[0])?;
let dst = self.fresh_ssa();
self.body
.push_str(&format!(" {dst} = xor i1 {a}, true\n"));
return Ok((dst, "i1".into()));
}
// User-Funktion?
if let Some(sig) = self.user_fns.get(name).cloned() {
let mut compiled_args = Vec::new();
for (a, exp_ty) in args.iter().zip(sig.params.iter()) {
let (v, vty) = self.lower_term(a)?;
if &vty != exp_ty {
return Err(CodegenError::Internal(format!(
"call `{name}` arg type mismatch: expected {exp_ty}, got {vty}"
)));
}
compiled_args.push((v, vty));
}
let arglist = compiled_args
.iter()
.map(|(v, t)| format!("{t} {v}"))
.collect::<Vec<_>>()
.join(", ");
let dst = self.fresh_ssa();
self.body.push_str(&format!(
" {dst} = call {ret} @ail_{name}({arglist})\n",
ret = sig.ret,
));
return Ok((dst, sig.ret));
}
Err(CodegenError::Internal(format!(
"unknown callee: `{name}`"
)))
}
fn lower_effect_op(&mut self, op: &str, args: &[Term]) -> Result<(String, String)> {
match op {
"io/print_int" => {
if args.len() != 1 {
return Err(CodegenError::Internal(
"io/print_int arity".into(),
));
}
let (v, vty) = self.lower_term(&args[0])?;
if vty != "i64" {
return Err(CodegenError::Internal(
"io/print_int needs i64".into(),
));
}
let fmt = self.intern_string("fmt_int", "%lld\n");
self.body.push_str(&format!(
" call i32 (ptr, ...) @printf(ptr @{fmt}, i64 {v})\n"
));
Ok(("0".into(), "i8".into()))
}
"io/print_bool" => {
if args.len() != 1 {
return Err(CodegenError::Internal(
"io/print_bool arity".into(),
));
}
let (v, vty) = self.lower_term(&args[0])?;
if vty != "i1" {
return Err(CodegenError::Internal(
"io/print_bool needs i1".into(),
));
}
// Drucke "true\n" oder "false\n".
let fmt_t = self.intern_string("fmt_true", "true\n");
let fmt_f = self.intern_string("fmt_false", "false\n");
let id = self.fresh_id();
let then_lbl = format!("ptbl_t.{id}");
let else_lbl = format!("ptbl_f.{id}");
let join_lbl = format!("ptbl_j.{id}");
self.body.push_str(&format!(
" br i1 {v}, label %{then_lbl}, label %{else_lbl}\n"
));
self.body.push_str(&format!("{then_lbl}:\n"));
self.body.push_str(&format!(
" call i32 (ptr, ...) @printf(ptr @{fmt_t})\n"
));
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{else_lbl}:\n"));
self.body.push_str(&format!(
" call i32 (ptr, ...) @printf(ptr @{fmt_f})\n"
));
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{join_lbl}:\n"));
Ok(("0".into(), "i8".into()))
}
other => Err(CodegenError::Internal(format!(
"unknown effect op: {other}"
))),
}
}
fn fresh_ssa(&mut self) -> String {
self.counter += 1;
format!("%v{}", self.counter)
}
fn fresh_id(&mut self) -> u64 {
self.counter += 1;
self.counter
}
/// Im MVP haben wir keinen verschachtelten Control-Flow innerhalb von
/// `then`/`else` von `if`, also entspricht das End-Label dem Block-Anfang.
/// Das ändert sich, sobald `if` rekursiv andere `if`s enthält — dann muss
/// das tatsächlich aktuelle Label am Phi-Punkt verwendet werden.
fn current_block_label_for_phi(&self, fallback: &str) -> String {
// Heuristik: scanne self.body rückwärts nach dem letzten Label-Header.
// Das ist robuster als anzunehmen, dass der ursprüngliche Block-Header
// noch der aktuelle ist.
for line in self.body.lines().rev() {
let line = line.trim_end();
if let Some(s) = line.strip_suffix(':') {
if !s.starts_with(' ') && !s.is_empty() && !s.contains(' ') {
return s.to_string();
}
}
}
fallback.to_string()
}
fn intern_string(&mut self, hint: &str, content: &str) -> String {
if let Some((name, _)) = self.strings.get(content) {
return name.clone();
}
let name = format!(".str_{}_{}", hint, self.str_counter);
self.str_counter += 1;
let len = c_byte_len(content);
self.strings
.insert(content.to_string(), (name.clone(), len));
name
}
}
fn llvm_type(t: &Type) -> Result<String> {
match t {
Type::Con { name } => match name.as_str() {
"Int" => Ok("i64".into()),
"Bool" => Ok("i1".into()),
"Unit" => Ok("i8".into()),
other => Err(CodegenError::UnsupportedType(other.into())),
},
other => Err(CodegenError::UnsupportedType(
ailang_core::pretty::type_to_string(other),
)),
}
}
fn builtin_binop(name: &str) -> Option<(&'static str, &'static str)> {
Some(match name {
"+" => ("add", "i64"),
"-" => ("sub", "i64"),
"*" => ("mul", "i64"),
"/" => ("sdiv", "i64"),
"%" => ("srem", "i64"),
"==" => ("icmp eq", "i1"),
"!=" => ("icmp ne", "i1"),
"<" => ("icmp slt", "i1"),
"<=" => ("icmp sle", "i1"),
">" => ("icmp sgt", "i1"),
">=" => ("icmp sge", "i1"),
_ => return None,
})
}
fn c_byte_len(s: &str) -> usize {
s.as_bytes().len() + 1 // + NUL
}
/// Escapt einen String für LLVM IR `c"..."`. Alle Bytes außerhalb von
/// 0x20..0x7E werden als `\HH` escapt; `"` und `\` ebenfalls. Endet mit `\00`.
fn default_triple() -> &'static str {
// Im MVP fragen wir den Compile-Host. Für Cross-Compilation müsste man das
// konfigurierbar machen — kein Bedarf jetzt.
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
"x86_64-pc-linux-gnu"
} else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
"arm64-apple-darwin"
} else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
"x86_64-apple-darwin"
} else if cfg!(target_arch = "aarch64") {
"aarch64-unknown-linux-gnu"
} else {
"x86_64-pc-linux-gnu"
}
}
fn ll_string_literal(s: &str) -> String {
let mut out = String::new();
for &b in s.as_bytes() {
match b {
b'"' => out.push_str("\\22"),
b'\\' => out.push_str("\\5C"),
0x20..=0x7E => out.push(b as char),
_ => out.push_str(&format!("\\{:02X}", b)),
}
}
out.push_str("\\00");
out
}
#[cfg(test)]
mod tests {
use super::*;
use ailang_core::SCHEMA;
#[test]
fn emits_arith_fn() {
let m = Module {
schema: SCHEMA.into(),
name: "t".into(),
imports: vec![],
defs: vec![Def::Fn(FnDef {
name: "add".into(),
ty: Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
},
params: vec!["a".into(), "b".into()],
body: Term::App {
callee: Box::new(Term::Var { name: "+".into() }),
args: vec![
Term::Var { name: "a".into() },
Term::Var { name: "b".into() },
],
},
doc: None,
})],
};
let ir = emit_ir(&m).unwrap();
assert!(ir.contains("define i64 @ail_add(i64 %arg_a, i64 %arg_b)"));
assert!(ir.contains("add i64 %arg_a, %arg_b"));
}
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "ailang-core"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
blake3.workspace = true
thiserror.workspace = true
indexmap.workspace = true
+151
View File
@@ -0,0 +1,151 @@
//! AST-Knoten. Serde-Layout entspricht dem JSON-Schema in `docs/DESIGN.md`.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Module {
pub schema: String,
pub name: String,
#[serde(default)]
pub imports: Vec<Import>,
pub defs: Vec<Def>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Import {
pub module: String,
#[serde(rename = "as", default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Def {
Fn(FnDef),
Const(ConstDef),
}
impl Def {
pub fn name(&self) -> &str {
match self {
Def::Fn(f) => &f.name,
Def::Const(c) => &c.name,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FnDef {
pub name: String,
#[serde(rename = "type")]
pub ty: Type,
pub params: Vec<String>,
pub body: Term,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstDef {
pub name: String,
#[serde(rename = "type")]
pub ty: Type,
pub value: Term,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "t", rename_all = "lowercase")]
pub enum Term {
Lit { lit: Literal },
Var { name: String },
App {
#[serde(rename = "fn")]
callee: Box<Term>,
args: Vec<Term>,
},
Let {
name: String,
value: Box<Term>,
body: Box<Term>,
},
If {
cond: Box<Term>,
then: Box<Term>,
#[serde(rename = "else")]
else_: Box<Term>,
},
Do {
op: String,
args: Vec<Term>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Literal {
Int { value: i64 },
Bool { value: bool },
Unit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "k", rename_all = "lowercase")]
pub enum Type {
Con {
name: String,
},
Fn {
params: Vec<Type>,
ret: Box<Type>,
#[serde(default)]
effects: Vec<String>,
},
Var {
name: String,
},
Forall {
vars: Vec<String>,
body: Box<Type>,
},
}
impl Type {
pub fn int() -> Type {
Type::Con { name: "Int".into() }
}
pub fn bool_() -> Type {
Type::Con { name: "Bool".into() }
}
pub fn unit() -> Type {
Type::Con { name: "Unit".into() }
}
}
impl PartialEq for Type {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Type::Con { name: a }, Type::Con { name: b }) => a == b,
(
Type::Fn { params: ap, ret: ar, effects: ae },
Type::Fn { params: bp, ret: br, effects: be },
) => {
ap == bp && ar == br && {
let mut a = ae.clone();
let mut b = be.clone();
a.sort();
b.sort();
a == b
}
}
(Type::Var { name: a }, Type::Var { name: b }) => a == b,
(
Type::Forall { vars: a, body: ab },
Type::Forall { vars: b, body: bb },
) => a == b && ab == bb,
_ => false,
}
}
}
impl Eq for Type {}
+85
View File
@@ -0,0 +1,85 @@
//! Kanonische JSON-Serialisierung.
//!
//! Schreibt JSON ohne Whitespace und mit lexikographisch sortierten Object-Keys.
//! Damit ist die Repräsentation deterministisch und für Hashing geeignet.
use std::io::Write;
/// Kanonische Bytes für ein beliebiges Serializable-Objekt.
pub fn to_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> {
let v = serde_json::to_value(value).expect("serializable");
let mut out = Vec::new();
write_value(&v, &mut out).expect("write to Vec");
out
}
fn write_value(v: &serde_json::Value, out: &mut Vec<u8>) -> std::io::Result<()> {
use serde_json::Value;
match v {
Value::Null => out.write_all(b"null"),
Value::Bool(true) => out.write_all(b"true"),
Value::Bool(false) => out.write_all(b"false"),
Value::Number(n) => out.write_all(n.to_string().as_bytes()),
Value::String(s) => {
let escaped = serde_json::to_string(s).expect("string serializable");
out.write_all(escaped.as_bytes())
}
Value::Array(arr) => {
out.write_all(b"[")?;
for (i, item) in arr.iter().enumerate() {
if i > 0 {
out.write_all(b",")?;
}
write_value(item, out)?;
}
out.write_all(b"]")
}
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
out.write_all(b"{")?;
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.write_all(b",")?;
}
let kj = serde_json::to_string(k).expect("key serializable");
out.write_all(kj.as_bytes())?;
out.write_all(b":")?;
write_value(&map[*k], out)?;
}
out.write_all(b"}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn sorts_keys() {
let v = json!({ "b": 1, "a": 2 });
let bytes = to_bytes(&v);
assert_eq!(std::str::from_utf8(&bytes).unwrap(), r#"{"a":2,"b":1}"#);
}
#[test]
fn nested_keys_sorted() {
let v = json!({ "z": { "y": 1, "x": [3, { "b": 2, "a": 1 }] } });
let bytes = to_bytes(&v);
assert_eq!(
std::str::from_utf8(&bytes).unwrap(),
r#"{"z":{"x":[3,{"a":1,"b":2}],"y":1}}"#
);
}
#[test]
fn no_whitespace() {
let v = json!({ "a": 1, "b": [1, 2, 3] });
let bytes = to_bytes(&v);
let s = std::str::from_utf8(&bytes).unwrap();
assert!(!s.contains(' '));
assert!(!s.contains('\n'));
}
}
+62
View File
@@ -0,0 +1,62 @@
//! Content-addressed Hashing für Definitionen.
//!
//! Hash = BLAKE3 über die kanonische JSON-Form (siehe `canonical`).
//! Das `hash`-Feld in der Eingabe wird vor dem Hashen entfernt.
use crate::ast::Def;
use crate::canonical;
/// 16-Hex-Zeichen (64 bit) Prefix des BLAKE3-Hashes.
/// Reicht für Eindeutigkeit innerhalb realistischer Codebases und ist
/// kompakt genug für visuelle Inspektion.
pub fn def_hash(def: &Def) -> String {
let bytes = canonical::to_bytes(def);
let h = blake3::hash(&bytes);
let hex = h.to_hex();
hex.as_str()[..16].to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::*;
fn sample_fn() -> Def {
Def::Fn(FnDef {
name: "add".into(),
ty: Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
},
params: vec!["a".into(), "b".into()],
body: Term::App {
callee: Box::new(Term::Var { name: "+".into() }),
args: vec![
Term::Var { name: "a".into() },
Term::Var { name: "b".into() },
],
},
doc: None,
})
}
#[test]
fn hash_is_stable() {
let h1 = def_hash(&sample_fn());
let h2 = def_hash(&sample_fn());
assert_eq!(h1, h2);
assert_eq!(h1.len(), 16);
}
#[test]
fn hash_changes_with_content() {
let mut def = sample_fn();
let h1 = def_hash(&def);
if let Def::Fn(ref mut f) = def {
f.name = "mul".into();
}
let h2 = def_hash(&def);
assert_ne!(h1, h2);
}
}
+40
View File
@@ -0,0 +1,40 @@
//! AILang Kerndatenmodell.
//!
//! Quelle einer AILang-Übersetzungseinheit ist ein `Module` als JSON. Dieses
//! Crate definiert das Schema, Serialisierung und content-addressed Hashing.
pub mod ast;
pub mod canonical;
pub mod hash;
pub mod pretty;
pub use ast::{ConstDef, Def, FnDef, Import, Literal, Module, Term, Type};
pub use hash::def_hash;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("schema mismatch: expected {expected:?}, got {got:?}")]
SchemaMismatch { expected: String, got: String },
#[error("json: {0}")]
Json(#[from] serde_json::Error),
#[error("io: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
pub const SCHEMA: &str = "ailang/v0";
pub fn load_module(path: &std::path::Path) -> Result<Module> {
let bytes = std::fs::read(path)?;
let module: Module = serde_json::from_slice(&bytes)?;
if module.schema != SCHEMA {
return Err(Error::SchemaMismatch {
expected: SCHEMA.to_string(),
got: module.schema,
});
}
Ok(module)
}
+255
View File
@@ -0,0 +1,255 @@
//! Pretty-Printer: AST → menschenlesbare Textform.
//!
//! Die Textform ist als Diff- und Review-Werkzeug gedacht. Die
//! kanonische Quelle bleibt die JSON-Form. Jede pretty-Ausgabe ist
//! deterministisch.
use crate::ast::*;
use std::fmt::Write;
pub fn module(m: &Module) -> String {
let mut s = String::new();
writeln!(s, "(module {}", m.name).unwrap();
if !m.imports.is_empty() {
for imp in &m.imports {
match &imp.alias {
Some(a) => writeln!(s, " (import {} as {})", imp.module, a).unwrap(),
None => writeln!(s, " (import {})", imp.module).unwrap(),
}
}
}
for (i, def) in m.defs.iter().enumerate() {
if i > 0 {
s.push('\n');
}
let body = def_block(def, 2);
s.push_str(&body);
s.push('\n');
}
s.push(')');
s.push('\n');
s
}
pub fn manifest(m: &Module) -> String {
let mut s = String::new();
writeln!(s, "module {}", m.name).unwrap();
let max_name = m.defs.iter().map(|d| d.name().len()).max().unwrap_or(0);
for def in &m.defs {
let h = crate::hash::def_hash(def);
let (kw, ty) = match def {
Def::Fn(f) => ("fn", type_to_string(&f.ty)),
Def::Const(c) => ("const", type_to_string(&c.ty)),
};
writeln!(
s,
" {kw:5} {name:<width$} :: {ty} [{h}]",
kw = kw,
name = def.name(),
width = max_name,
ty = ty,
h = h,
)
.unwrap();
}
s
}
fn def_block(def: &Def, indent: usize) -> String {
let pad = " ".repeat(indent);
match def {
Def::Fn(f) => {
let params = if f.params.is_empty() {
"[]".to_string()
} else {
format!("[{}]", f.params.join(" "))
};
let mut s = format!(
"{pad}(fn {name} :: {ty} {params}\n",
pad = pad,
name = f.name,
ty = type_to_string(&f.ty),
params = params,
);
s.push_str(&term_block(&f.body, indent + 2));
s.push(')');
s
}
Def::Const(c) => {
let mut s = format!(
"{pad}(const {name} :: {ty}\n",
pad = pad,
name = c.name,
ty = type_to_string(&c.ty),
);
s.push_str(&term_block(&c.value, indent + 2));
s.push(')');
s
}
}
}
fn term_block(t: &Term, indent: usize) -> String {
let pad = " ".repeat(indent);
match t {
Term::Lit { lit } => format!("{pad}{}", lit_to_string(lit)),
Term::Var { name } => format!("{pad}{name}"),
Term::App { callee, args } => {
let mut s = format!("{pad}(");
s.push_str(&term_inline(callee));
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
Term::Let { name, value, body } => {
let mut s = format!("{pad}(let {name}\n");
s.push_str(&term_block(value, indent + 2));
s.push('\n');
s.push_str(&term_block(body, indent + 2));
s.push(')');
s
}
Term::If { cond, then, else_ } => {
let mut s = format!("{pad}(if\n");
s.push_str(&term_block(cond, indent + 2));
s.push('\n');
s.push_str(&term_block(then, indent + 2));
s.push('\n');
s.push_str(&term_block(else_, indent + 2));
s.push(')');
s
}
Term::Do { op, args } => {
let mut s = format!("{pad}(do {op}");
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
}
}
fn term_inline(t: &Term) -> String {
match t {
Term::Lit { lit } => lit_to_string(lit),
Term::Var { name } => name.clone(),
Term::App { callee, args } => {
let mut s = String::from("(");
s.push_str(&term_inline(callee));
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
Term::Do { op, args } => {
let mut s = format!("(do {op}");
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
// Strukturelle Terms in Inline-Form rekursiv schwer; fallback:
Term::Let { name, value, body } => {
format!(
"(let {name} {} {})",
term_inline(value),
term_inline(body)
)
}
Term::If { cond, then, else_ } => {
format!(
"(if {} {} {})",
term_inline(cond),
term_inline(then),
term_inline(else_)
)
}
}
}
fn lit_to_string(l: &Literal) -> String {
match l {
Literal::Int { value } => value.to_string(),
Literal::Bool { value } => value.to_string(),
Literal::Unit => "()".to_string(),
}
}
pub fn type_to_string(t: &Type) -> String {
match t {
Type::Con { name } => name.clone(),
Type::Var { name } => name.clone(),
Type::Fn { params, ret, effects } => {
let p = params
.iter()
.map(type_to_string)
.collect::<Vec<_>>()
.join(", ");
let eff = if effects.is_empty() {
String::new()
} else {
format!(" !{}", effects.join(","))
};
format!("({p}) -> {ret}{eff}", ret = type_to_string(ret))
}
Type::Forall { vars, body } => {
format!("forall {}. {}", vars.join(" "), type_to_string(body))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_module() -> Module {
Module {
schema: crate::SCHEMA.into(),
name: "sample".into(),
imports: vec![],
defs: vec![
Def::Fn(FnDef {
name: "add".into(),
ty: Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
},
params: vec!["a".into(), "b".into()],
body: Term::App {
callee: Box::new(Term::Var { name: "+".into() }),
args: vec![
Term::Var { name: "a".into() },
Term::Var { name: "b".into() },
],
},
doc: None,
}),
],
}
}
#[test]
fn pretty_print_does_not_panic() {
let s = module(&sample_module());
assert!(s.contains("(module sample"));
assert!(s.contains("(fn add"));
assert!(s.contains("(+ a b)"));
}
#[test]
fn manifest_contains_type_and_hash() {
let s = manifest(&sample_module());
assert!(s.contains("add"));
assert!(s.contains("(Int, Int) -> Int"));
}
}
+172
View File
@@ -0,0 +1,172 @@
# AILang — Designentscheidungen
Dieses Dokument hält die Kernentscheidungen für AILang fest. Es ist mein Vertrag mit
mir selbst über künftige Iterationen. Kürzungen statt Wachstum bevorzugen.
## Zielsetzung
AILang ist eine Programmiersprache für LLM-Autoren. Sie wird zu LLVM IR kompiliert.
Performance: nativ, ohne GC für den MVP.
Optimiert für:
- **Maschinenlesbarkeit** statt menschlicher Ergonomie. Quelle ist strukturiert.
- **Lokales Reasoning.** Jede Definition trägt ihren vollständigen Typ und ihre Effekte.
- **Beweisbarkeit.** Reine Kernsprache, explizite Effekte, optionale Refinements.
- **Robustheit gegen Halluzinationen.** Symbole sind hashbar; Tools können Existenz
verifizieren, ohne Kontextfenster zu verbrauchen.
## Entscheidung 1: Quelle = Daten, nicht Text
Ein Modul ist ein JSON-Objekt mit einem festen Schema. Es gibt keinen Parser für
Freitext. Tippfehler in Bezeichnern werden zu Hash-Lookup-Fehlern, die der Compiler
direkt vorschlägt zu fixen.
Eine Textform existiert (`.ail`, S-Expression-artig), aber nur als bidirektionale
Projektion der JSON-Form. Sie ist für Menschen-Reviews und Diffs gedacht.
**Kanonisches Format:** `.ail.json` mit deterministischer Schlüsselreihenfolge.
## Entscheidung 2: Content-addressed Definitionen
Jede Top-Level-Definition hat einen `hash`-Wert (BLAKE3 über kanonisches JSON ohne
das `hash`-Feld selbst). Verweise zwischen Definitionen erfolgen primär per Name —
Namen sind für Lesbarkeit. Der Hash ist die kanonische Identität.
Vorteile:
- Refactoring durch Hinzufügen neuer Defs, nicht durch In-place-Änderung. Alte
Versionen bleiben aufrufbar, bis manuell entfernt.
- Caching von Typcheck-Ergebnissen und Codegen pro Hash.
- Diffs zeigen exakt, welche Def sich geändert hat.
## Entscheidung 3: Reine Kernsprache + algebraische Effekte
Default sind totale, reine Funktionen. Effekte werden als Set im Funktionstyp
deklariert: `(Int) -> Int ![IO]`. Die Effektmenge ist row-polymorph
(`![IO | r]`). Im MVP sind nur die Effekte `IO` und `Diverge` (für Endlosschleifen)
verbaut.
Dies ist die wichtigste LLM-Eigenschaft: Wenn ich eine Funktion lese, kann ich
ihrer Signatur trauen, ohne den Body zu lesen.
## Entscheidung 4: Hindley-Milner + optionale Refinements
MVP: HM mit Let-Polymorphismus. Alle Typen sind inferierbar, müssen aber im
Top-Level immer explizit annotiert sein (für lokales Reasoning).
Später: Refinement-Annotationen, die zu SMT escalieren. `(i: Int | i >= 0)`. Vom
Anfang an im AST vorgesehen, aber im MVP einfach als opake Strings durchgereicht.
## Entscheidung 5: LLVM IR als Text emittieren
Statt `inkwell` oder `llvm-sys`: AILang erzeugt `.ll`-Dateien als Strings und
übergibt an `clang` zum Linken.
Begründung:
- LLVM-IR-Textsyntax ist über Versionen weitgehend stabil.
- Keine Build-Abhängigkeit von einer bestimmten libllvm-Version.
- Generierter Code ist trivial inspizierbar, was Debugging massiv vereinfacht.
- LLM kann generierten IR direkt lesen, was bei opaken Bibliothekscalls schwerer ist.
Trade-off: keine Inline-Optimierungen über die LLVM-API. Wir setzen auf
`clang -O2` als Standard-Pipeline.
## Datenmodell (MVP)
### Module
```jsonc
{
"schema": "ailang/v0",
"name": "<id>",
"imports": [{ "module": "<id>", "as": "<id>" }],
"defs": [Def...]
}
```
### Def
`kind ∈ { "fn", "type", "effect", "const" }`. Im MVP nur `fn` und `const`.
```jsonc
{
"kind": "fn",
"name": "<id>",
"type": Type,
"params": ["<id>"...],
"body": Term,
"doc": "<optional string>"
}
```
### Term (Expression)
```jsonc
{ "t": "lit", "lit": { "kind": "int" | "bool" | "unit", "value": ... } }
{ "t": "var", "name": "<id>" }
{ "t": "app", "fn": Term, "args": [Term...] }
{ "t": "let", "name": "<id>", "value": Term, "body": Term }
{ "t": "if", "cond": Term, "then": Term, "else": Term }
{ "t": "do", "op": "<eff>/<op>", "args": [Term...] }
```
`do` ist im MVP nur ein direkter Aufruf eines Built-in-Effekt-Ops (kein Handler).
### Type
```jsonc
{ "k": "con", "name": "Int" }
{ "k": "con", "name": "Bool" }
{ "k": "con", "name": "Unit" }
{ "k": "fn", "params": [Type...], "ret": Type, "effects": ["IO"...] }
{ "k": "var", "name": "a" }
{ "k": "forall", "vars": ["a"...], "body": Type }
```
## Pipeline
```
.ail.json ─┐
├─ load + validate schema
├─ resolve names + assign hashes
├─ typecheck (HM, effect rows)
├─ lower to MIR (SSA-ähnlich, named SSA-Werte)
├─ emit LLVM IR (.ll)
└─ clang -O2 *.ll -o binary
```
## CLI
```
ail check <module.ail.json> — Lädt, validiert, typecheckt
ail manifest <module.ail.json> — Tabelle: name :: type !effects [hash]
ail describe <module> <name> — Detail einer Definition
ail render <module> — JSON → Pretty-Text
ail parse <module.ail> — Pretty-Text → JSON (für Bootstrapping)
ail emit-ir <module> — schreibt .ll
ail build <module> — komplette Pipeline → Binary
```
## Verifikation und Korrektheit (über Zyklen)
1. **Snapshot-Tests** für Pretty-Printer und IR-Emit. Diff macht Regressions sofort
sichtbar.
2. **Property-Tests** für Roundtrip JSON ↔ Pretty.
3. **End-to-End-Tests** für `examples/` mit erwartetem Programmoutput.
4. **Hash-Stabilität**: Test stellt sicher, dass dieselbe Def stets denselben Hash
produziert.
5. **CI-Pin** der Outputs in `tests/expected/`.
## Was MVP NICHT ist
- Keine ADTs / Pattern Matching (Phase 2).
- Keine Closures / höhere Funktionen (Phase 2).
- Keine Effekt-Handler (Phase 3).
- Keine Refinements / SMT (Phase 4).
- Kein Modulsystem über Imports hinaus (Phase 2).
- Keine Strings als first-class. Nur ints + bools + unit.
Der MVP ist erfolgreich, wenn `examples/sum.ail.json` ein Binary erzeugt, das die
Summe 1..10 = 55 druckt.
+17
View File
@@ -0,0 +1,17 @@
# JOURNAL
Chronologische Notizen für mich. Nicht jede Änderung; nur Entscheidungen,
Hindernisse, Beobachtungen, die zukünftige Iterationen brauchen.
## 2026-05-07 — Tag 0
- Repo initialisiert. Auftrag in `CLAUDE.md`: LLM-native Sprache, LLVM-Backend.
- Designentscheidungen festgehalten in `docs/DESIGN.md`.
- Toolchain: `rustc 1.94`, `llvm-config 22.1.3`, `clang` vorhanden.
- Entschieden gegen `inkwell` zugunsten LLVM-IR-Text-Emit. Begründung im DESIGN.md.
- Workspace-Layout:
- `crates/ailang-core` — AST, Type, Hash, JSON-Schema
- `crates/ailang-check` — Typchecker (kommt später)
- `crates/ailang-codegen` — Lowering + LLVM IR Emit
- `crates/ail` — CLI
- MVP-Ziel: `examples/sum.ail.json` → Binary, das 55 druckt.
+76
View File
@@ -0,0 +1,76 @@
{
"schema": "ailang/v0",
"name": "sum",
"imports": [],
"defs": [
{
"kind": "fn",
"name": "sum",
"type": {
"k": "fn",
"params": [{ "k": "con", "name": "Int" }],
"ret": { "k": "con", "name": "Int" },
"effects": []
},
"params": ["n"],
"doc": "rekursive Summe 0..=n",
"body": {
"t": "if",
"cond": {
"t": "app",
"fn": { "t": "var", "name": "==" },
"args": [
{ "t": "var", "name": "n" },
{ "t": "lit", "lit": { "kind": "int", "value": 0 } }
]
},
"then": { "t": "lit", "lit": { "kind": "int", "value": 0 } },
"else": {
"t": "app",
"fn": { "t": "var", "name": "+" },
"args": [
{ "t": "var", "name": "n" },
{
"t": "app",
"fn": { "t": "var", "name": "sum" },
"args": [
{
"t": "app",
"fn": { "t": "var", "name": "-" },
"args": [
{ "t": "var", "name": "n" },
{ "t": "lit", "lit": { "kind": "int", "value": 1 } }
]
}
]
}
]
}
}
},
{
"kind": "fn",
"name": "main",
"type": {
"k": "fn",
"params": [],
"ret": { "k": "con", "name": "Unit" },
"effects": ["IO"]
},
"params": [],
"body": {
"t": "do",
"op": "io/print_int",
"args": [
{
"t": "app",
"fn": { "t": "var", "name": "sum" },
"args": [
{ "t": "lit", "lit": { "kind": "int", "value": 10 } }
]
}
]
}
}
]
}