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
+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");
}