Files
AILang/docs/DESIGN.md
T
Brummel 2fbcdba0b1 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>
2026-05-07 10:18:32 +02:00

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)

  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.