From 2fbcdba0b1b9ca1f589aeab052db77ed60e41514 Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 7 May 2026 10:18:32 +0200 Subject: [PATCH] MVP: AILang-Sprache mit JSON-AST, Typchecker, LLVM-IR-Backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 3 + CLAUDE.md | 12 + Cargo.lock | 408 +++++++++++++++++++ Cargo.toml | 32 ++ crates/ail/Cargo.toml | 17 + crates/ail/src/main.rs | 137 +++++++ crates/ail/tests/e2e.rs | 44 +++ crates/ailang-check/Cargo.toml | 10 + crates/ailang-check/src/builtins.rs | 75 ++++ crates/ailang-check/src/lib.rs | 433 +++++++++++++++++++++ crates/ailang-codegen/Cargo.toml | 11 + crates/ailang-codegen/src/lib.rs | 581 ++++++++++++++++++++++++++++ crates/ailang-core/Cargo.toml | 12 + crates/ailang-core/src/ast.rs | 151 ++++++++ crates/ailang-core/src/canonical.rs | 85 ++++ crates/ailang-core/src/hash.rs | 62 +++ crates/ailang-core/src/lib.rs | 40 ++ crates/ailang-core/src/pretty.rs | 255 ++++++++++++ docs/DESIGN.md | 172 ++++++++ docs/JOURNAL.md | 17 + examples/sum.ail.json | 76 ++++ 21 files changed, 2633 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/ail/Cargo.toml create mode 100644 crates/ail/src/main.rs create mode 100644 crates/ail/tests/e2e.rs create mode 100644 crates/ailang-check/Cargo.toml create mode 100644 crates/ailang-check/src/builtins.rs create mode 100644 crates/ailang-check/src/lib.rs create mode 100644 crates/ailang-codegen/Cargo.toml create mode 100644 crates/ailang-codegen/src/lib.rs create mode 100644 crates/ailang-core/Cargo.toml create mode 100644 crates/ailang-core/src/ast.rs create mode 100644 crates/ailang-core/src/canonical.rs create mode 100644 crates/ailang-core/src/hash.rs create mode 100644 crates/ailang-core/src/lib.rs create mode 100644 crates/ailang-core/src/pretty.rs create mode 100644 docs/DESIGN.md create mode 100644 docs/JOURNAL.md create mode 100644 examples/sum.ail.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c4cc47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +*.ll.o +/build diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2d9db4b --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..42f059f --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..403f0bc --- /dev/null +++ b/Cargo.toml @@ -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 diff --git a/crates/ail/Cargo.toml b/crates/ail/Cargo.toml new file mode 100644 index 0000000..f0667b3 --- /dev/null +++ b/crates/ail/Cargo.toml @@ -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 diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs new file mode 100644 index 0000000..6f862af --- /dev/null +++ b/crates/ail/src/main.rs @@ -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, + }, + /// Komplette Pipeline: check + emit-ir + clang -> Binary. + Build { + path: PathBuf, + #[arg(short, long)] + out: Option, + /// 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(()) +} diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs new file mode 100644 index 0000000..03fb2a9 --- /dev/null +++ b/crates/ail/tests/e2e.rs @@ -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"); +} diff --git a/crates/ailang-check/Cargo.toml b/crates/ailang-check/Cargo.toml new file mode 100644 index 0000000..9872bc2 --- /dev/null +++ b/crates/ailang-check/Cargo.toml @@ -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 diff --git a/crates/ailang-check/src/builtins.rs b/crates/ailang-check/src/builtins.rs new file mode 100644 index 0000000..ad0ebc0 --- /dev/null +++ b/crates/ailang-check/src/builtins.rs @@ -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, + 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]"), + ] +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs new file mode 100644 index 0000000..0ab616b --- /dev/null +++ b/crates/ailang-check/src/lib.rs @@ -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), + + #[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), +} + +type Result = std::result::Result; + +/// 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, +} + +pub fn check(m: &Module) -> Result { + 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 = 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, + effects: &mut BTreeSet, + in_def: &str, +) -> Result { + 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(), + _ => "".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, + pub effect_ops: IndexMap, +} + +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")); + } +} diff --git a/crates/ailang-codegen/Cargo.toml b/crates/ailang-codegen/Cargo.toml new file mode 100644 index 0000000..5c3845b --- /dev/null +++ b/crates/ailang-codegen/Cargo.toml @@ -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 diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs new file mode 100644 index 0000000..261b3bf --- /dev/null +++ b/crates/ailang-codegen/src/lib.rs @@ -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), + + #[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 = std::result::Result; + +pub fn emit_ir(m: &Module) -> Result { + 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, + /// 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, +} + +#[derive(Debug, Clone)] +struct FnSig { + params: Vec, // 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> = + 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 = + param_tys.iter().map(llvm_type).collect::>()?; + 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_ + 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::>() + .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 { + 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")); + } +} diff --git a/crates/ailang-core/Cargo.toml b/crates/ailang-core/Cargo.toml new file mode 100644 index 0000000..344cb1e --- /dev/null +++ b/crates/ailang-core/Cargo.toml @@ -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 diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs new file mode 100644 index 0000000..e8a8777 --- /dev/null +++ b/crates/ailang-core/src/ast.rs @@ -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, + pub defs: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Import { + pub module: String, + #[serde(rename = "as", default, skip_serializing_if = "Option::is_none")] + pub alias: Option, +} + +#[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, + pub body: Term, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub doc: Option, +} + +#[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, +} + +#[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, + args: Vec, + }, + Let { + name: String, + value: Box, + body: Box, + }, + If { + cond: Box, + then: Box, + #[serde(rename = "else")] + else_: Box, + }, + Do { + op: String, + args: Vec, + }, +} + +#[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, + ret: Box, + #[serde(default)] + effects: Vec, + }, + Var { + name: String, + }, + Forall { + vars: Vec, + body: Box, + }, +} + +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 {} diff --git a/crates/ailang-core/src/canonical.rs b/crates/ailang-core/src/canonical.rs new file mode 100644 index 0000000..4a582f9 --- /dev/null +++ b/crates/ailang-core/src/canonical.rs @@ -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(value: &T) -> Vec { + 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) -> 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')); + } +} diff --git a/crates/ailang-core/src/hash.rs b/crates/ailang-core/src/hash.rs new file mode 100644 index 0000000..2fac342 --- /dev/null +++ b/crates/ailang-core/src/hash.rs @@ -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); + } +} diff --git a/crates/ailang-core/src/lib.rs b/crates/ailang-core/src/lib.rs new file mode 100644 index 0000000..09651c9 --- /dev/null +++ b/crates/ailang-core/src/lib.rs @@ -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 = std::result::Result; + +pub const SCHEMA: &str = "ailang/v0"; + +pub fn load_module(path: &std::path::Path) -> Result { + 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) +} diff --git a/crates/ailang-core/src/pretty.rs b/crates/ailang-core/src/pretty.rs new file mode 100644 index 0000000..391d1b1 --- /dev/null +++ b/crates/ailang-core/src/pretty.rs @@ -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: 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::>() + .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")); + } +} diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..c4045ce --- /dev/null +++ b/docs/DESIGN.md @@ -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": "", + "imports": [{ "module": "", "as": "" }], + "defs": [Def...] +} +``` + +### Def + +`kind ∈ { "fn", "type", "effect", "const" }`. Im MVP nur `fn` und `const`. + +```jsonc +{ + "kind": "fn", + "name": "", + "type": Type, + "params": [""...], + "body": Term, + "doc": "" +} +``` + +### Term (Expression) + +```jsonc +{ "t": "lit", "lit": { "kind": "int" | "bool" | "unit", "value": ... } } +{ "t": "var", "name": "" } +{ "t": "app", "fn": Term, "args": [Term...] } +{ "t": "let", "name": "", "value": Term, "body": Term } +{ "t": "if", "cond": Term, "then": Term, "else": Term } +{ "t": "do", "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 — Lädt, validiert, typecheckt +ail manifest — Tabelle: name :: type !effects [hash] +ail describe — Detail einer Definition +ail render — JSON → Pretty-Text +ail parse — Pretty-Text → JSON (für Bootstrapping) +ail emit-ir — schreibt .ll +ail build — 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. diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md new file mode 100644 index 0000000..d34f41e --- /dev/null +++ b/docs/JOURNAL.md @@ -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. diff --git a/examples/sum.ail.json b/examples/sum.ail.json new file mode 100644 index 0000000..a480b8b --- /dev/null +++ b/examples/sum.ail.json @@ -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 } } + ] + } + ] + } + } + ] +}