Files
AILang/docs/DESIGN.md
T
Brummel a20ab93c66 Iter 5c: Cross-Module-Codegen
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.
2026-05-07 11:39:59 +02:00

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 --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. 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-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

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