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,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
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user