b1dbafc6f2
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>.
230 lines
8.7 KiB
Markdown
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.
|