Files
AILang/docs/DESIGN.md
T
Brummel b1dbafc6f2 Doku-Drift fixen + Iter-5d-JOURNAL nachreichen
JOURNAL bekommt fehlenden Iter-5d-Eintrag und Iter-5-Architektur-
Review-Notiz mit Iter-6-Plan (Aufräumarbeiten). DESIGN.md korrigiert
zwei Drifts vom Architekt-Befund: @main ist i32 (C-/LLVM-ABI), nicht
i64; String-Globals tragen Hint @.str_<modul>_<hint>_<idx>.
2026-05-07 11:50:49 +02:00

230 lines
8.7 KiB
Markdown

# 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 `--json` fü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. Siehe `agents/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. Konstanten ebenfalls (`@ail_<modul>_<const>`). Globale
String-Literale tragen einen kurzen Hint zur Lesbarkeit:
`@.str_<modul>_<hint>_<idx>` (z. B. `@.str_sum_fmt_int_0`). Eintrittspunkt
ist eine `define i32 @main()`-Trampoline (C-/LLVM-ABI), die
`@ail_<entry-modul>_main()` aufruft. `source_filename` existiert genau
einmal pro Workspace und trägt den Eintrittsmodulnamen
(`<entry-modul>.ail`).
## 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-name` mit `ctx: { "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
```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.