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