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>
5.7 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.
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
{
"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.