Symbol-Mangling-Schema einheitlich auf @ail_<modul>_<def> umgestellt (auch für Single-Modul-Programme), String-Globals als @.str_<modul>_<idx>. main bleibt LLVM-/C-ABI-Eintrittspunkt und ist ein Trampoline auf @ail_<entry>_main. lower_workspace emittiert eine einzige .ll für den ganzen Workspace, alphabetisch nach Modulname, Cross-Module-Calls über Import-Map aufgelöst. ail build / ail emit-ir laufen jetzt durch den Workspace-Pfad. IR-Snapshots regeneriert, neuer ws_main-Snapshot. E2E-Test workspace_build_runs_imported_fn prüft, dass das Binary die importierte Funktion korrekt aufruft. Schuld #19 (source_filename) durch einheitliches <entry>.ail-Schema geschlossen.
8.5 KiB
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.
Projekt-Ökosystem
AILang ist nicht nur eine Sprache, sondern ein Ökosystem. Die Sprache an sich ist nur dann wertvoll, wenn das Drumherum sie für ihren Zielnutzer (LLM-Autoren) benutzbar, prüfbar und erweiterbar macht. Das Repo enthält daher mehrere gleichwertige Komponenten — keine davon ist optional, alle entwickeln sich parallel mit der Sprache:
- Sprachkern (
crates/ailang-core,crates/ailang-check,crates/ailang-codegen): AST, Typsystem, Codegen. - CLI (
crates/ail): Toolchain für Tooling-Konsumenten —manifest,describe,deps,check,build, etc., bevorzugt mit--jsonfür maschinelle Konsumption. - Beispiele (
examples/): kanonische.ail.json-Programme. Sie sind Spezifikationsanker, nicht Demos — die E2E-Suite hängt an ihnen. - Agenten (
agents/): spezialisierte Sub-Prompts (Implementer, Architect, Tester, Debugger), die das Projekt-eigene LLM-Tooling bilden. Sie sind versionierter Bestandteil des Repos. Sieheagents/README.md. - Doku (
docs/): DESIGN.md (was und warum), JOURNAL.md (Verlauf). - Tests: Unit pro Crate + E2E in
crates/ail/tests/e2e.rs. Jeder neue Compiler-Pfad braucht einen Test, sonst zählt das Feature nicht als fertig.
Wenn die Sprache wächst, wachsen diese Komponenten mit. Neue Tools, die das
LLM-Tooling stärken (z. B. ail diff, IR-Snapshot-Diffs, neue Agenten),
gehören explizit ins Ökosystem-Inventar dieses Abschnitts und werden hier
ergänzt, sobald sie etabliert sind.
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.
Mangling-Schema (Iter 5c)
Alle AILang-Funktionen werden zu @ail_<modul>_<def> gemangelt — auch im
Single-Modul-Fall. Globale Strings/Konstanten zu @.str_<modul>_<idx>
bzw. @ail_<modul>_<def>. Eintrittspunkt ist eine define i64 @main()-
Trampoline, die @ail_<entry-modul>_main() aufruft. source_filename
existiert genau einmal pro Workspace und trägt den Eintrittsmodulnamen.
Konvention: Qualifizierte Cross-Module-Verweise (Iter 5b)
Cross-Module-Aufrufe nutzen keinen neuen AST-Knoten. Stattdessen ist ein
Term::Var { name } mit genau einem Punkt im Namen ein qualifizierter
Verweis: <prefix>.<def>.
<prefix>ist ein Import-Alias (import { module: "X", as: "<prefix>" }) oder, falls ohne Alias importiert, der Modulname selbst.<def>ist der Name einer Top-Level-Definition im Zielmodul.- Def-Namen DÜRFEN keinen Punkt enthalten — der Typchecker meldet
invalid-def-namemitctx: { "reason": "contains-dot" }. - Der Workspace-Loader (Iter 5a) findet alle erreichbaren Module; der
Typchecker (Iter 5b,
check_workspace) löst Punkt-Namen über die Import-Map auf. Diagnostic-Codes:unknown-module(Prefix nicht importiert),unknown-import(Modul gefunden, Def nicht).
Hash-Stabilität: kein neuer AST-Knoten, keine umbenannten Felder — alle bisherigen Modul-Hashes bleiben bitidentisch.
Datenmodell (MVP)
Module
{
"schema": "ailang/v0",
"name": "<id>",
"imports": [{ "module": "<id>", "as": "<id>" }],
"defs": [Def...]
}
Def
kind ∈ { "fn", "type", "effect", "const" }. Im MVP nur fn und const.
{
"kind": "fn",
"name": "<id>",
"type": Type,
"params": ["<id>"...],
"body": Term,
"doc": "<optional string>"
}
Term (Expression)
{ "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
{ "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)
- Snapshot-Tests für Pretty-Printer und IR-Emit. Diff macht Regressions sofort sichtbar.
- Property-Tests für Roundtrip JSON ↔ Pretty.
- End-to-End-Tests für
examples/mit erwartetem Programmoutput. - Hash-Stabilität: Test stellt sicher, dass dieselbe Def stets denselben Hash produziert.
- 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.