21606c9340
- AST: Def::Type mit Ctors; Term::Ctor (Konstruktion) und Term::Match
mit Arm/Pattern. Patterns: Wild, Var, Lit, Ctor { ctor, fields } —
Sub-Patterns im MVP auf Var/Wild beschränkt.
- Typchecker: Type-Registry, ctor_index für O(1)-Resolution, Pattern-
Bindings, Exhaustiveness-Check gegen volle Konstruktormenge plus
Negativ-Tests.
- Codegen: Boxed-Heap-Layout via malloc; Tag in Offset 0, Felder ab
Offset 8 in 8-Byte-Slots. Match lowert zu load tag + switch + Phi
am Join. Default-Block ist unreachable, wenn vom Typchecker geprüft.
- examples/list.ail.json: rekursive Int-Liste mit sum_list via match.
E2E-Test + Exhaustiveness-Tests. 19/19 Tests grün.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
323 lines
11 KiB
Rust
323 lines
11 KiB
Rust
//! `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,
|
|
#[arg(long)]
|
|
json: bool,
|
|
},
|
|
/// 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,
|
|
},
|
|
/// Listet, welche Symbole jede Definition aufruft (statisch).
|
|
Deps {
|
|
path: PathBuf,
|
|
/// Nur für ein Symbol; ohne Argument: für alle.
|
|
#[arg(long)]
|
|
of: Option<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 {
|
|
#[arg(long)]
|
|
json: bool,
|
|
},
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
match cli.cmd {
|
|
Cmd::Manifest { path, json } => {
|
|
let m = ailang_core::load_module(&path)?;
|
|
if json {
|
|
let entries: Vec<_> = m
|
|
.defs
|
|
.iter()
|
|
.map(|d| {
|
|
let h = ailang_core::def_hash(d);
|
|
let (kind, ty, effects) = match d {
|
|
ailang_core::Def::Fn(f) => {
|
|
let effects = match &f.ty {
|
|
ailang_core::Type::Fn { effects, .. } => effects.clone(),
|
|
_ => vec![],
|
|
};
|
|
(
|
|
"fn",
|
|
ailang_core::pretty::type_to_string(&f.ty),
|
|
effects,
|
|
)
|
|
}
|
|
ailang_core::Def::Const(c) => (
|
|
"const",
|
|
ailang_core::pretty::type_to_string(&c.ty),
|
|
vec![],
|
|
),
|
|
ailang_core::Def::Type(t) => {
|
|
let s = t
|
|
.ctors
|
|
.iter()
|
|
.map(|c| {
|
|
if c.fields.is_empty() {
|
|
c.name.clone()
|
|
} else {
|
|
format!(
|
|
"{}({})",
|
|
c.name,
|
|
c.fields
|
|
.iter()
|
|
.map(ailang_core::pretty::type_to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
)
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" | ");
|
|
("type", s, vec![])
|
|
}
|
|
};
|
|
serde_json::json!({
|
|
"name": d.name(),
|
|
"kind": kind,
|
|
"type": ty,
|
|
"effects": effects,
|
|
"hash": h,
|
|
})
|
|
})
|
|
.collect();
|
|
let out = serde_json::json!({
|
|
"module": m.name,
|
|
"schema": m.schema,
|
|
"symbols": entries,
|
|
});
|
|
println!("{}", serde_json::to_string_pretty(&out)?);
|
|
} else {
|
|
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 { json } => {
|
|
let list = ailang_check::builtins::list();
|
|
if json {
|
|
let arr: Vec<_> = list
|
|
.iter()
|
|
.map(|(n, s)| serde_json::json!({ "name": n, "sig": s }))
|
|
.collect();
|
|
println!("{}", serde_json::to_string_pretty(&arr)?);
|
|
} else {
|
|
for (n, sig) in list {
|
|
println!("{n:<16} {sig}");
|
|
}
|
|
}
|
|
}
|
|
Cmd::Deps { path, of, json } => {
|
|
let m = ailang_core::load_module(&path)?;
|
|
let mut entries = Vec::new();
|
|
for d in &m.defs {
|
|
if let Some(filter) = &of {
|
|
if d.name() != filter {
|
|
continue;
|
|
}
|
|
}
|
|
let mut refs: Vec<String> = collect_refs(d).into_iter().collect();
|
|
refs.sort();
|
|
entries.push((d.name().to_string(), refs));
|
|
}
|
|
if json {
|
|
let arr: Vec<_> = entries
|
|
.iter()
|
|
.map(|(n, r)| serde_json::json!({ "name": n, "refs": r }))
|
|
.collect();
|
|
println!("{}", serde_json::to_string_pretty(&arr)?);
|
|
} else {
|
|
for (n, refs) in entries {
|
|
if refs.is_empty() {
|
|
println!("{n:>20} -");
|
|
} else {
|
|
println!("{n:>20} -> {}", refs.join(", "));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
|
|
let mut out = std::collections::BTreeSet::new();
|
|
match def {
|
|
ailang_core::Def::Fn(f) => walk_term(&f.body, &mut out),
|
|
ailang_core::Def::Const(c) => walk_term(&c.value, &mut out),
|
|
ailang_core::Def::Type(td) => {
|
|
// Eine Typedef referenziert die Typen ihrer Felder.
|
|
for c in &td.ctors {
|
|
for ft in &c.fields {
|
|
if let ailang_core::Type::Con { name } = ft {
|
|
out.insert(format!("type:{name}"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn walk_term(t: &ailang_core::Term, out: &mut std::collections::BTreeSet<String>) {
|
|
use ailang_core::Term;
|
|
match t {
|
|
Term::Lit { .. } => {}
|
|
Term::Var { name } => {
|
|
out.insert(name.clone());
|
|
}
|
|
Term::App { callee, args } => {
|
|
walk_term(callee, out);
|
|
for a in args {
|
|
walk_term(a, out);
|
|
}
|
|
}
|
|
Term::Let { value, body, .. } => {
|
|
walk_term(value, out);
|
|
walk_term(body, out);
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
walk_term(cond, out);
|
|
walk_term(then, out);
|
|
walk_term(else_, out);
|
|
}
|
|
Term::Do { op, args } => {
|
|
// Effekt-Ops als `effect:io/print_int` markieren, damit man sie
|
|
// von normalen Funktionsaufrufen trennen kann.
|
|
out.insert(format!("effect:{op}"));
|
|
for a in args {
|
|
walk_term(a, out);
|
|
}
|
|
}
|
|
Term::Ctor { type_name, ctor, args } => {
|
|
out.insert(format!("ctor:{type_name}/{ctor}"));
|
|
for a in args {
|
|
walk_term(a, out);
|
|
}
|
|
}
|
|
Term::Match { scrutinee, arms } => {
|
|
walk_term(scrutinee, out);
|
|
for arm in arms {
|
|
if let ailang_core::ast::Pattern::Ctor { ctor, .. } = &arm.pat {
|
|
out.insert(format!("ctor:{ctor}"));
|
|
}
|
|
walk_term(&arm.body, out);
|
|
}
|
|
}
|
|
}
|
|
}
|