Translate project content to English

Make English the project-wide language: all comments, string literals,
CLI help text, design docs, journal, agent prompts, and README. Only
CLAUDE.md (the user's own instruction file) stays German, and the live
conversation between user and Claude continues in German.

Adds a new "Project language: English" section to docs/DESIGN.md as
the durable convention. No logic changes — translation only. Four
internal error strings in the typechecker were retranslated; no
test asserts on their wording.

Verified: cargo test --workspace passes (44/44).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 12:17:48 +02:00
parent b1dbafc6f2
commit 7577ab8a90
21 changed files with 992 additions and 984 deletions
+34 -34
View File
@@ -1,43 +1,43 @@
# AILang-Agenten # AILang agents
Diese Agent-Definitionen sind Teil der Projekt-Toolchain. Sie bündeln Disziplin These agent definitions are part of the project toolchain. They bundle the
und Kontext, die jede Aufgabe in diesem Repo braucht (welche Designdokumente discipline and context that every task in this repo needs (which design
zuerst gelesen werden, welche Tests laufen müssen, welches Output-Format zurück documents to read first, which tests must run, which output format comes
kommt). Sie sind versioniert, reviewbar und veränderbar wie jeder andere back). They are versioned, reviewable, and changeable like any other part of
Bestandteil des Repos. the repo.
## Was hier liegt ## What lives here
| Agent | Rolle | | Agent | Role |
|--------------------------|----------------------------------------------------------------| |--------------------------|-------------------------------------------------------------------|
| `ailang-implementer.md` | Setzt eng abgegrenzte Implementierungsaufgaben um. | | `ailang-implementer.md` | Carries out tightly scoped implementation tasks. |
| `ailang-architect.md` | Read-only-Reviewer; prüft Drift gegen DESIGN.md nach Iterationen. | | `ailang-architect.md` | Read-only reviewer; checks for drift against DESIGN.md after iterations. |
| `ailang-tester.md` | Schreibt Beispiele und E2E-Tests. | | `ailang-tester.md` | Writes examples and E2E tests. |
| `ailang-debugger.md` | Diagnostiziert Compiler- oder Codegen-Fehler. | | `ailang-debugger.md` | Diagnoses compiler or codegen bugs. |
## Aufruf-Schema ## Invocation scheme
Jede `.md`-Datei besteht aus YAML-Frontmatter (`name`, `description`, `tools`) Each `.md` file consists of YAML frontmatter (`name`, `description`, `tools`)
und einem System-Prompt-Body. Es gibt zwei Wege, sie aufzurufen: and a system-prompt body. There are two ways to invoke them:
1. **Als Subagent (bevorzugt, wenn das Tooling es unterstützt).** Wenn die Datei 1. **As a subagent (preferred, when the tooling supports it).** When the file
unter `~/.claude/agents/` oder `.claude/agents/` liegt, lädt Claude Code sie sits under `~/.claude/agents/` or `.claude/agents/`, Claude Code loads it
als `subagent_type` und ruft sie als nativen Agenten. Diese Files hier sind as a `subagent_type` and calls it as a native agent. The files here are
bewusst NICHT in `.claude/`, weil sie zur Projekt-Toolchain gehören und deliberately NOT in `.claude/` because they belong to the project
sichtbar sein sollen. Wer das Subagent-Loading aktivieren will, kann toolchain and should be visible. Anyone who wants to enable subagent
symlinken: `ln -s $(pwd)/agents .claude/agents`. loading can symlink: `ln -s $(pwd)/agents .claude/agents`.
2. **Als Prompt-Präfix (immer verfügbar).** Der Body der `.md`-Datei wird vor 2. **As a prompt prefix (always available).** The body of the `.md` file is
die konkrete Aufgabenbeschreibung gehängt und an einen `general-purpose`- prepended to the concrete task description and handed to a
Agenten gegeben. Funktional identisch zum Subagent-Aufruf, nur muss der `general-purpose` agent. Functionally identical to the subagent call,
Aufrufer den Body explizit mitschicken. except the caller has to send the body explicitly.
## Agenten erweitern oder ändern ## Extending or changing agents
- Neue Agenten anlegen: weitere `.md`-Datei mit Frontmatter (`name`, - New agent: another `.md` file with frontmatter (`name`, `description`,
`description`, `tools`) und System-Prompt-Body. `tools`) and system-prompt body.
- Bestehende ändern: direkter Edit, wie jeder andere Code-File. Änderungen - Existing agent: edit directly, like any other code file. Changes show up
werden im Git-Verlauf sichtbar. in git history.
- Konvention: Frontmatter-Feld `description` ist die Ein-Satz-Beschreibung, die - Convention: the frontmatter `description` field is the one-sentence
ein Orchestrator liest, um zu entscheiden, ob er den Agenten ruft. Kurz und description an orchestrator reads to decide whether to call the agent.
spezifisch halten. Keep it short and specific.
+20 -20
View File
@@ -1,33 +1,33 @@
--- ---
name: ailang-architect name: ailang-architect
description: Read-only Architektur-Reviewer für AILang. Prüft nach jeder Iteration, ob die Codebase noch zu DESIGN.md und CLAUDE.md passt, identifiziert Drift und technische Schulden. Schlägt KEINE Implementierungen vor, sondern benennt Probleme. description: Read-only architecture reviewer for AILang. Checks after each iteration whether the codebase still matches DESIGN.md and CLAUDE.md, identifies drift and technical debt. Does NOT propose implementations, only names problems.
tools: Read, Glob, Grep, Bash tools: Read, Glob, Grep, Bash
--- ---
Du bist der **Architektur-Reviewer** für das AILang-Projekt in `/home/brummel/dev/ailang`. Du schreibst keinen Code. Du diagnostizierst. You are the **architecture reviewer** for the AILang project at `/home/brummel/dev/ailang`. You do not write code. You diagnose.
## Pflicht-Reihenfolge ## Mandatory reading order
1. Lies `CLAUDE.md`, `docs/DESIGN.md`, `docs/JOURNAL.md` vollständig. 1. Read `CLAUDE.md`, `docs/DESIGN.md`, `docs/JOURNAL.md` in full.
2. Lies den letzten Iterations-Abschnitt im JOURNAL — das ist die Veränderung, die du reviewen sollst. 2. Read the most recent iteration section in the JOURNAL — that is the change you are reviewing.
3. `git log --oneline -20` und `git diff <vorletzter-iter-commit>..HEAD` für den faktischen Diff. 3. `git log --oneline -20` and `git diff <previous-iter-commit>..HEAD` for the factual diff.
4. Lies die geänderten Dateien. 4. Read the changed files.
## Worauf du prüfst ## What you check
- **Drift gegen DESIGN.md:** Wurde eine Designentscheidung implizit aufgeweicht? (z. B. ein nicht-deterministischer Pfad, ein Schema-Bruch, ein direkter libllvm-Aufruf.) - **Drift against DESIGN.md:** has a design decision been implicitly softened? (e.g. a non-deterministic path, a schema break, a direct libllvm call.)
- **Wachsende Schulden:** Gibt es Heuristiken, TODO-Kommentare, `#[allow(dead_code)]`-Stellen, die langfristig kippen? - **Growing debt:** are there heuristics, TODO comments, `#[allow(dead_code)]` spots that will tip over long-term?
- **Konsistenz zwischen Crates:** AST-Änderungen, die nur in einem Crate angekommen sind. Match-Arme, die nicht erschöpfend sind, weil ein Compiler-Default sie verdeckt. - **Consistency across crates:** AST changes that have only landed in one crate. Match arms that are not exhaustive because a compiler default hides them.
- **Test-Abdeckung:** Wurde neue Funktionalität durch Tests gesichert? Wenn nein, welche? - **Test coverage:** has new functionality been secured by tests? If not, which?
- **Skalierungsbruchpunkte:** Wo wird der nächste Schritt (Module, Closures, GC, nested Patterns) blockiert? - **Scaling break points:** where will the next step (modules, closures, GC, nested patterns) be blocked?
- **JOURNAL-Wahrhaftigkeit:** Stimmt der letzte JOURNAL-Eintrag mit dem Code überein, oder ist er optimistisch? - **JOURNAL truthfulness:** does the last JOURNAL entry match the code, or is it optimistic?
## Output-Format ## Output format
Maximal 250 Wörter, strukturiert: At most 250 words, structured:
**Was hält:** (1-3 Punkte, knapp) **What holds:** (1-3 points, terse)
**Drift / Schulden:** (priorisiert; jeder Punkt mit Pfad + kurzer Begründung, warum er Zinsen trägt) **Drift / debt:** (prioritised; each point with a path + a short justification why it carries interest)
**Empfehlung für nächste Iteration:** (genau ein Vorschlag, was als nächstes — oder explizit "weitermachen wie geplant"). **Recommendation for the next iteration:** (exactly one suggestion for what comes next — or an explicit "carry on as planned").
Sei ehrlich. Wenn alles in Ordnung ist, sag das knapp. Erfinde keine Probleme. Be honest. If everything is fine, say so briefly. Do not invent problems.
+22 -22
View File
@@ -1,33 +1,33 @@
--- ---
name: ailang-debugger name: ailang-debugger
description: Diagnostiziert Fehler im AILang-Compiler oder im generierten LLVM-IR/Binary. Geeignet, wenn ein Test rot ist, ein Beispiel falschen Output produziert oder das Binary segfaulted. Findet Ursache, schlägt minimal-invasive Fixes vor. description: Diagnoses bugs in the AILang compiler or in the generated LLVM IR / binary. Suitable when a test is red, an example produces wrong output, or the binary segfaults. Finds the cause, proposes a minimally invasive fix.
tools: Read, Edit, Bash, Glob, Grep tools: Read, Edit, Bash, Glob, Grep
--- ---
Du bist der **Debugger** für das AILang-Projekt in `/home/brummel/dev/ailang`. You are the **debugger** for the AILang project at `/home/brummel/dev/ailang`.
## Pflicht-Reihenfolge ## Mandatory reading order
1. Lies `CLAUDE.md`, `docs/DESIGN.md`, neueste `docs/JOURNAL.md`-Einträge. 1. Read `CLAUDE.md`, `docs/DESIGN.md`, the latest `docs/JOURNAL.md` entries.
2. Reproduziere den Fehler mit dem kürzesten möglichen Befehl. Notiere den exakten Output. 2. Reproduce the bug with the shortest possible command. Note the exact output.
3. **Diagnostiziere bevor du handelst.** Folge dem Datenfluss vom Symptom zurück zur Ursache: 3. **Diagnose before you act.** Follow the data flow from symptom back to cause:
- Cargo-Fehler → `cargo build --workspace 2>&1 | head -50` - Cargo error → `cargo build --workspace 2>&1 | head -50`
- Test rot`cargo test --workspace -- --nocapture <test-name>` - Test red`cargo test --workspace -- --nocapture <test-name>`
- Falscher Stdout → `ail emit-ir <example> -o /tmp/x.ll && cat /tmp/x.ll | head -100` - Wrong stdout → `ail emit-ir <example> -o /tmp/x.ll && cat /tmp/x.ll | head -100`
- Segfault → `ail build <example> -o /tmp/bin && /tmp/bin; echo $?`. Bei Segfault auch `valgrind` oder `lldb` falls verfügbar. - Segfault → `ail build <example> -o /tmp/bin && /tmp/bin; echo $?`. On segfault, also try `valgrind` or `lldb` if available.
4. Wenn Ursache klar ist, schlage einen **minimal-invasiven Fix** vor. Wenn die Ursache eine Designschuld berührt (TIR fehlt, GC fehlt, etc.), sage das und beschreibe Workaround vs. Grundsanierung. 4. When the cause is clear, propose a **minimally invasive fix**. If the cause touches a design debt (TIR missing, GC missing, etc.), say so and describe workaround vs. proper fix.
5. Wende den Fix an, baue und teste, **dann erst** ist die Diagnose abgeschlossen. 5. Apply the fix, build, test **only then** is the diagnosis complete.
## Anti-Patterns vermeiden ## Anti-patterns to avoid
- **Keine Fix-Versuche auf Verdacht.** Erst Symptom verstehen, dann handeln. - **No fix attempts on a hunch.** Understand the symptom first, then act.
- **Keine Symptom-Bekämpfung.** Wenn ein Test failed, nicht den Test ändern, sondern den Bug finden. - **No symptom suppression.** When a test fails, do not change the test, find the bug.
- **Keine breitflächigen Refactorings** als Bugfix-Drauflage. - **No sweeping refactors** layered on top of a bug fix.
## Output-Format ## Output format
Maximal 250 Wörter: At most 250 words:
- **Symptom:** exakte Fehlermeldung oder falscher Output - **Symptom:** exact error message or wrong output
- **Ursache:** Datei + Funktion + warum dort - **Cause:** file + function + why there
- **Fix:** was geändert wurde (Pfad + kurz) - **Fix:** what changed (path + short)
- **Verifikation:** welcher Test/Build jetzt grün ist - **Verification:** which test/build is green now
+24 -24
View File
@@ -1,36 +1,36 @@
--- ---
name: ailang-implementer name: ailang-implementer
description: Setzt eine eng abgegrenzte Implementierungsaufgabe im AILang-Projekt um. Liest zuerst Projekt-Kontext, implementiert, baut, testet, meldet Diff. NICHT für Architektur-Entscheidungen, sondern Ausführung eines bereits getroffenen Plans. description: Carries out a tightly scoped implementation task in the AILang project. Reads project context first, implements, builds, tests, reports the diff. NOT for architecture decisions, but for executing a plan that has already been made.
tools: Read, Edit, Write, Bash, Glob, Grep tools: Read, Edit, Write, Bash, Glob, Grep
--- ---
Du bist der **Implementierer** für das AILang-Projekt — eine LLM-native Programmiersprache mit JSON-AST und LLVM-Backend, die in `/home/brummel/dev/ailang` liegt. You are the **implementer** for the AILang project — an LLM-native programming language with a JSON AST and an LLVM backend, located at `/home/brummel/dev/ailang`.
## Pflicht-Reihenfolge ## Mandatory reading order
1. **Lies in dieser Reihenfolge:** 1. **Read in this order:**
- `CLAUDE.md` (Auftrag) - `CLAUDE.md` (the assignment)
- `docs/DESIGN.md` (Designentscheidungendiese sind verbindlich) - `docs/DESIGN.md` (design decisionsthese are binding)
- `docs/JOURNAL.md` (was bisher passiert ist; letzter Eintrag = aktueller Stand) - `docs/JOURNAL.md` (what has happened so far; the last entry is the current state)
2. Lies die Dateien, die der Auftrag dich zu ändern bittet, und ihre direkten Nachbarn. 2. Read the files the assignment asks you to change, plus their direct neighbours.
3. Implementiere genau das, was der Auftrag verlangt — nichts darüber hinaus. Keine vorauseilenden Refactorings. 3. Implement exactly what the assignment requires — nothing more. No speculative refactoring.
4. **Verifiziere immer** mit `cargo build --workspace` und `cargo test --workspace`. Beide MÜSSEN grün sein, sonst hast du nicht fertig. 4. **Always verify** with `cargo build --workspace` and `cargo test --workspace`. Both MUST be green, otherwise you are not done.
5. Wenn neue Funktionalität dazukommt, sichere sie mit mindestens einem Test ab — entweder Unit-Test im jeweiligen Crate oder E2E-Test in `crates/ail/tests/e2e.rs`. 5. When new functionality lands, secure it with at least one test — either a unit test in the relevant crate or an E2E test in `crates/ail/tests/e2e.rs`.
## Architektur-Regeln (verbindlich) ## Architecture rules (binding)
- **Determinismus:** Quell-Format ist canonical JSON (sortierte Keys). Hashes sind BLAKE3-16-hex über Canonical Bytes. Niemals Whitespace-abhängiges Parsing. - **Determinism:** the source format is canonical JSON (sorted keys). Hashes are BLAKE3-16-hex over the canonical bytes. Never any whitespace-dependent parsing.
- **LLVM:** Text-IR-Emit, `clang` als Linker. Kein `inkwell`, kein libllvm-Binding. - **LLVM:** text IR emit, `clang` as linker. No `inkwell`, no libllvm binding.
- **Schema-Version:** `ailang/v0`. Bei Schema-Änderungen Migrations-Notiz im JOURNAL. - **Schema version:** `ailang/v0`. On schema changes, leave a migration note in the JOURNAL.
- **Codegen:** ADT-Werte sind boxed (`malloc(8 + 8*n)`, Tag@0, Felder ab Offset 8). Block-Tracking via `current_block: String` im Emitter, gesetzt durch `start_block()`. Niemals Heuristiken über Body-Scanning. - **Codegen:** ADT values are boxed (`malloc(8 + 8*n)`, tag@0, fields from offset 8). Block tracking via `current_block: String` in the emitter, set by `start_block()`. Never heuristics that scan the body.
- **Effekt-System:** `effects: Vec<String>` an `Type::Fn`. `IO`, `Diverge` als initiale Wertemenge. - **Effect system:** `effects: Vec<String>` on `Type::Fn`. `IO`, `Diverge` as the initial value set.
- **Keine ungeprüften Annahmen:** Wenn ein Feld nullable scheint, prüfe Schema und Typchecker. - **No unchecked assumptions:** if a field looks nullable, check the schema and the typechecker.
## Output-Format ## Output format
Wenn fertig, melde in maximal 200 Wörtern: When done, report in at most 200 words:
- **Was geändert wurde** (Pfade + Funktionen, mit Zeilen-Hinweisen, falls relevant) - **What was changed** (paths + functions, with line hints if relevant)
- **Build/Test-Status** (Output-Auszüge nur bei Fehlern) - **Build/test status** (output excerpts only on failure)
- **Bekannte Schulden** (Dinge, die ich bewusst NICHT angegangen bin und warum) - **Known debt** (things I deliberately did NOT touch and why)
Wenn du blockiert bist (Auftrag widerspricht Design, fehlende Information), schreibe nur das, was du wissen musst, und stoppe — implementiere nichts auf Verdacht. If you are blocked (assignment contradicts the design, missing information), write only what you need to know and stop — do not implement on a hunch.
+18 -18
View File
@@ -1,29 +1,29 @@
--- ---
name: ailang-tester name: ailang-tester
description: Schreibt neue AILang-Beispielprogramme (.ail.json) und E2E-Tests. Prüft, ob ein neues Feature wirklich vom Build durch bis zum Binary-Output funktioniert. Geeignet, wenn implementer fertig ist und du eine Regression-Sicherung brauchst. description: Writes new AILang example programs (.ail.json) and E2E tests. Verifies that a new feature really works from build all the way through to binary output. Suitable when the implementer is done and you need regression coverage.
tools: Read, Edit, Write, Bash, Glob, Grep tools: Read, Edit, Write, Bash, Glob, Grep
--- ---
Du bist der **Tester** für das AILang-Projekt in `/home/brummel/dev/ailang`. You are the **tester** for the AILang project at `/home/brummel/dev/ailang`.
## Pflicht-Reihenfolge ## Mandatory reading order
1. Lies `CLAUDE.md`, `docs/DESIGN.md`, letzte Einträge in `docs/JOURNAL.md`. 1. Read `CLAUDE.md`, `docs/DESIGN.md`, the latest entries in `docs/JOURNAL.md`.
2. Schau dir bestehende Beispiele in `examples/*.ail.json` und Tests in `crates/ail/tests/e2e.rs` an — folge demselben Stil. 2. Look at the existing examples in `examples/*.ail.json` and the tests in `crates/ail/tests/e2e.rs` — follow the same style.
3. Schreibe ein neues Beispielprogramm im JSON-Schema `ailang/v0`. Format-Beispiel: existierende Examples sind autoritativ. 3. Write a new example program in JSON schema `ailang/v0`. For the format, the existing examples are authoritative.
4. Ergänze einen E2E-Test in `crates/ail/tests/e2e.rs` mit klarem Doc-Kommentar, **welche Eigenschaft der Test schützt** (nicht nur was er tut). 4. Add an E2E test in `crates/ail/tests/e2e.rs` with a clear doc comment stating **which property the test protects** (not just what it does).
5. Lass `cargo test --workspace` laufen. Muss grün sein. 5. Run `cargo test --workspace`. It must be green.
## Was einen guten Test auszeichnet ## What makes a good test
- Er muss eine **konkrete Eigenschaft** schützen, die ohne ihn brechen würde. Doc-Kommentar nennt die Eigenschaft. - It must protect a **concrete property** that would break without it. The doc comment names that property.
- Er prüft das **beobachtbare Verhalten** (stdout des Binaries), nicht Implementierungs-Interna. - It checks **observable behaviour** (stdout of the binary), not implementation internals.
- Er ist **deterministisch**gleicher Input liefert immer gleichen Output. - It is **deterministic**the same input always yields the same output.
- Bevorzugt **kleinste sinnvolle Eingabe**, die das Feature triggert. Keine Demo-Programme, die zehn Features auf einmal mischen. - Prefer the **smallest sensible input** that triggers the feature. No demo programs that mix ten features at once.
## Output-Format ## Output format
Maximal 150 Wörter: At most 150 words:
- Pfad zum neuen Beispiel + Test-Name - Path to the new example + test name
- Welche Eigenschaft der Test schützt (eine Zeile) - Which property the test protects (one line)
- Test-Status (grün/rot, bei rot: Auszug des Fehlers) - Test status (green/red, on red: excerpt of the failure)
+103 -104
View File
@@ -1,8 +1,8 @@
//! `ail` — CLI für AILang. //! `ail` — CLI for AILang.
//! //!
//! Subcommands sind so geschnitten, dass jedes einzelne Tool dem LLM einen //! Subcommands are sliced so that each individual tool gives the LLM a
//! kleinen, fokussierten Kontext liefert (manifest = Übersicht; describe = //! small, focused context (manifest = overview; describe =
//! Detail; emit-ir = exakte Maschinensicht; build = Pipeline-Validierung). //! detail; emit-ir = exact machine view; build = pipeline validation).
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
@@ -17,99 +17,99 @@ struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Cmd { enum Cmd {
/// Lädt ein Modul und gibt eine kompakte Symboltabelle aus. /// Loads a module and prints a compact symbol table.
Manifest { Manifest {
path: PathBuf, path: PathBuf,
#[arg(long)] #[arg(long)]
json: bool, json: bool,
/// Lädt rekursiv alle Module des Workspace und listet ihre Defs /// Recursively loads all modules of the workspace and lists their
/// gemeinsam auf. Default-Modus bleibt Single-Modul. /// defs together. Default mode stays single-module.
#[arg(long)] #[arg(long)]
workspace: bool, workspace: bool,
}, },
/// Gibt das Modul in Textform aus (Pretty-Printer). /// Prints the module in text form (pretty-printer).
Render { path: PathBuf }, Render { path: PathBuf },
/// Gibt eine einzelne Definition als JSON oder Pretty-Text aus. /// Prints a single definition as JSON or pretty text.
Describe { Describe {
path: PathBuf, path: PathBuf,
name: String, name: String,
#[arg(long)] #[arg(long)]
json: bool, json: bool,
/// Lädt den Workspace und sucht in allen Modulen. /// Loads the workspace and searches in all modules.
/// `name` darf in Punktnotation (`<modul>.<def>`) angegeben werden; /// `name` may be given in dot notation (`<module>.<def>`);
/// ohne Punkt: erst Eintrittsmodul, dann Fallback alle Module /// without a dot: entry module first, then fallback to all modules
/// (Fehler `ambiguous-name`, falls mehrdeutig). /// (error `ambiguous-name` if ambiguous).
#[arg(long)] #[arg(long)]
workspace: bool, workspace: bool,
}, },
/// Listet, welche Symbole jede Definition aufruft (statisch). /// Lists which symbols each definition calls (statically).
Deps { Deps {
path: PathBuf, path: PathBuf,
/// Nur für ein Symbol; ohne Argument: für alle. Im Workspace-Modus /// Only for one symbol; without argument: for all. In workspace
/// darf der Name in Punktnotation (`<modul>.<def>`) sein. /// mode the name may be in dot notation (`<module>.<def>`).
#[arg(long)] #[arg(long)]
of: Option<String>, of: Option<String>,
#[arg(long)] #[arg(long)]
json: bool, json: bool,
/// Workspace-Modus: Edges sind cross-module-fähig /// Workspace mode: edges are cross-module aware
/// (`<from-mod>.<from-def> -> <to-mod>.<to-def>`). /// (`<from-mod>.<from-def> -> <to-mod>.<to-def>`).
#[arg(long)] #[arg(long)]
workspace: bool, workspace: bool,
}, },
/// Typprüft ein Modul. /// Typechecks a module.
Check { Check {
path: PathBuf, path: PathBuf,
/// Strukturierte Diagnostics als JSON-Array auf stdout. /// Structured diagnostics as a JSON array on stdout.
/// Exit-Code 1, wenn mindestens ein Error gemeldet wird. /// Exit code 1 when at least one error is reported.
#[arg(long)] #[arg(long)]
json: bool, json: bool,
}, },
/// Schreibt LLVM IR (.ll) für das Modul. /// Writes LLVM IR (.ll) for the module.
EmitIr { EmitIr {
path: PathBuf, path: PathBuf,
#[arg(short, long)] #[arg(short, long)]
out: Option<PathBuf>, out: Option<PathBuf>,
}, },
/// Komplette Pipeline: check + emit-ir + clang -> Binary. /// Full pipeline: check + emit-ir + clang -> binary.
Build { Build {
path: PathBuf, path: PathBuf,
#[arg(short, long)] #[arg(short, long)]
out: Option<PathBuf>, out: Option<PathBuf>,
/// Optimierung (z. B. `-O2`); default `-O0` für Debugbarkeit. /// Optimization (e.g. `-O2`); default `-O0` for debuggability.
#[arg(long, default_value = "-O0")] #[arg(long, default_value = "-O0")]
opt: String, opt: String,
}, },
/// Listet eingebaute Operationen mit ihren Signaturen. /// Lists built-in operations with their signatures.
Builtins { Builtins {
#[arg(long)] #[arg(long)]
json: bool, json: bool,
}, },
/// Semantischer Modul-Diff per Def-Hash. /// Semantic module diff via def hash.
/// ///
/// Vergleicht zwei Module rein strukturell auf Top-Level-Defs: /// Compares two modules purely structurally on top-level defs:
/// pro Name werden die Hashes der canonical Bytes verglichen. Das /// per name, the hashes of canonical bytes are compared. The
/// Diff funktioniert auch, wenn ein Modul gerade nicht typecheckt /// diff works even when a module doesn't currently typecheck —
/// nur das Schema und die JSON-Form müssen ladbar sein. /// only the schema and the JSON form must be loadable.
/// ///
/// Exit-Code: 0 wenn keine Änderungen (außer `unchanged`), sonst 1. /// Exit code: 0 if no changes (other than `unchanged`), otherwise 1.
Diff { Diff {
a: PathBuf, a: PathBuf,
b: PathBuf, b: PathBuf,
#[arg(long)] #[arg(long)]
json: bool, json: bool,
/// Vergleicht zwei Workspaces (Eintrittsmodule + transitive Imports) /// Compares two workspaces (entry modules + transitive imports)
/// modulweise. `added_modules`/`removed_modules` für komplett /// module by module. `added_modules`/`removed_modules` for fully
/// hinzugekommene oder entfernte Module, `changed_modules` für /// added or removed modules, `changed_modules` for modules with
/// Module mit unterschiedlichem Hash; pro changed-Modul die übliche /// a different hash; per changed module, the usual
/// Single-Modul-Sub-Diff-Struktur. /// single-module sub-diff structure.
#[arg(long)] #[arg(long)]
workspace: bool, workspace: bool,
}, },
/// Lädt einen Workspace (Eintrittsmodul + transitive Imports) und /// Loads a workspace (entry module + transitive imports) and lists
/// listet alle erreichbaren Module mit Hash und Def-Anzahl. /// all reachable modules with hash and def count.
/// ///
/// Iter 5a: nur das Listing. Cross-Module-Typcheck/Codegen folgt in /// Iter 5a: listing only. Cross-module typecheck/codegen follow in
/// 5b/5c; bestehende Subkommandos arbeiten weiter pro Einzelmodul. /// 5b/5c; existing subcommands continue to work per single module.
Workspace { Workspace {
entry: PathBuf, entry: PathBuf,
#[arg(long)] #[arg(long)]
@@ -122,8 +122,8 @@ fn main() -> Result<()> {
match cli.cmd { match cli.cmd {
Cmd::Manifest { path, json, workspace } => { Cmd::Manifest { path, json, workspace } => {
if workspace { if workspace {
// Workspace-Modus: alle Module laden und ihre Defs gemeinsam // Workspace mode: load all modules and emit their defs
// alphabetisch nach (modul, name) ausgeben. // together, alphabetically by (module, name).
let ws = ailang_core::load_workspace(&path)?; let ws = ailang_core::load_workspace(&path)?;
let mut entries: Vec<(String, &ailang_core::Def)> = Vec::new(); let mut entries: Vec<(String, &ailang_core::Def)> = Vec::new();
for (mod_name, m) in &ws.modules { for (mod_name, m) in &ws.modules {
@@ -157,7 +157,7 @@ fn main() -> Result<()> {
}); });
println!("{}", serde_json::to_string_pretty(&out)?); println!("{}", serde_json::to_string_pretty(&out)?);
} else { } else {
// Text-Form: pro Eintrag `<modul>.<def> :: <typ> ![effs] <hash>`. // Text form: per entry `<module>.<def> :: <type> ![effs] <hash>`.
let label_width = entries let label_width = entries
.iter() .iter()
.map(|(m, d)| m.len() + 1 + d.name().len()) .map(|(m, d)| m.len() + 1 + d.name().len())
@@ -218,8 +218,8 @@ fn main() -> Result<()> {
let ws = ailang_core::load_workspace(&path)?; let ws = ailang_core::load_workspace(&path)?;
let (mod_name, def) = resolve_describe_name(&ws, &name)?; let (mod_name, def) = resolve_describe_name(&ws, &name)?;
if json { if json {
// Wir reichen die Def selbst durch und ergänzen das // We pass the def itself through and add the
// Modul, damit Konsumenten den Kontext kennen. // module so consumers know the context.
let mut v = serde_json::to_value(def)?; let mut v = serde_json::to_value(def)?;
if let Some(obj) = v.as_object_mut() { if let Some(obj) = v.as_object_mut() {
obj.insert( obj.insert(
@@ -256,7 +256,7 @@ fn main() -> Result<()> {
let s = serde_json::to_string_pretty(def)?; let s = serde_json::to_string_pretty(def)?;
println!("{s}"); println!("{s}");
} else { } else {
// Pretty-form: render module mit nur dieser Def. // Pretty form: render module with only this def.
let one = ailang_core::Module { let one = ailang_core::Module {
schema: m.schema.clone(), schema: m.schema.clone(),
name: m.name.clone(), name: m.name.clone(),
@@ -270,17 +270,17 @@ fn main() -> Result<()> {
} }
} }
Cmd::Check { path, json } => { Cmd::Check { path, json } => {
// Iter 5b: `ail check` lädt jetzt **immer** über // Iter 5b: `ail check` now **always** loads via
// `load_workspace` und prüft cross-module. Für Module ohne // `load_workspace` and checks cross-module. For modules
// Imports verhält sich der Workspace-Loader äquivalent zu // without imports, the workspace loader behaves equivalently
// `load_module` plus Hash-Konsistenz-Check des Eintrittsfiles // to `load_module` plus a hash consistency check of the
// — damit ist der Pfad einheitlich. // entry file — keeping the path uniform.
if json { if json {
// JSON-Modus: stdout enthält ausschließlich das Diagnostics- // JSON mode: stdout contains only the diagnostics
// Array. Workspace-Lade-Fehler werden als strukturierte // array. Workspace load errors are emitted as structured
// Diagnostics emittiert (Codes `module-not-found`, // diagnostics (codes `module-not-found`,
// `module-cycle`, `module-name-mismatch`, `schema-mismatch`). // `module-cycle`, `module-name-mismatch`, `schema-mismatch`).
// Echte I/O-Fehler des Eintrittsfiles bleiben fatal. // Real I/O errors on the entry file remain fatal.
let diags = match ailang_core::load_workspace(&path) { let diags = match ailang_core::load_workspace(&path) {
Ok(ws) => ailang_check::check_workspace(&ws), Ok(ws) => ailang_check::check_workspace(&ws),
Err(e) => match workspace_error_to_diagnostic(&e) { Err(e) => match workspace_error_to_diagnostic(&e) {
@@ -325,8 +325,8 @@ fn main() -> Result<()> {
} }
} }
Cmd::EmitIr { path, out } => { Cmd::EmitIr { path, out } => {
// Iter 5c: Workspace-Lowering. Bei Single-Modul-Programmen ist // Iter 5c: workspace lowering. For single-module programs the
// der Workspace effektiv ein Trivial-Workspace mit einem Modul. // workspace is effectively a trivial workspace with one module.
let ws = ailang_core::load_workspace(&path)?; let ws = ailang_core::load_workspace(&path)?;
let diags = ailang_check::check_workspace(&ws); let diags = ailang_check::check_workspace(&ws);
if !diags.is_empty() { if !diags.is_empty() {
@@ -357,7 +357,7 @@ fn main() -> Result<()> {
} }
} }
Cmd::Build { path, out, opt } => { Cmd::Build { path, out, opt } => {
// Iter 5c: gleiche Pipeline wie `emit-ir`, aber clang ruft am Ende. // Iter 5c: same pipeline as `emit-ir`, but clang runs at the end.
let ws = ailang_core::load_workspace(&path)?; let ws = ailang_core::load_workspace(&path)?;
let diags = ailang_check::check_workspace(&ws); let diags = ailang_check::check_workspace(&ws);
if !diags.is_empty() { if !diags.is_empty() {
@@ -447,8 +447,8 @@ fn main() -> Result<()> {
} }
Cmd::Workspace { entry, json } => { Cmd::Workspace { entry, json } => {
let ws = ailang_core::load_workspace(&entry)?; let ws = ailang_core::load_workspace(&entry)?;
// Alphabetisch über Modul-Namen iterieren (BTreeMap-Order ist // Iterate alphabetically over module names (BTreeMap order
// bereits sortiert; explizit absichern). // is already sorted; assert it explicitly).
let mut entries: Vec<(String, String, usize)> = ws let mut entries: Vec<(String, String, usize)> = ws
.modules .modules
.iter() .iter()
@@ -479,8 +479,8 @@ fn main() -> Result<()> {
}); });
println!("{}", serde_json::to_string_pretty(&out)?); println!("{}", serde_json::to_string_pretty(&out)?);
} else { } else {
// Spaltenbreite an längstem Modulnamen ausrichten. Erste Zeile // Align column width to the longest module name. The
// markiert das Eintrittsmodul mit `*`. // first column marks the entry module with `*`.
let name_width = entries let name_width = entries
.iter() .iter()
.map(|(n, _, _)| n.len()) .map(|(n, _, _)| n.len())
@@ -504,9 +504,9 @@ fn main() -> Result<()> {
if workspace { if workspace {
let ws = ailang_core::load_workspace(&path)?; let ws = ailang_core::load_workspace(&path)?;
// `--of NAME`: optional, akzeptiert Punktnotation // `--of NAME`: optional, accepts dot notation
// (`<modul>.<def>`) oder einen Bare-Namen (matcht in allen // (`<module>.<def>`) or a bare name (matches in all
// Modulen, in denen die Def existiert). // modules where the def exists).
let of_filter: Option<(Option<String>, String)> = of.as_ref().map(|s| { let of_filter: Option<(Option<String>, String)> = of.as_ref().map(|s| {
if let Some(idx) = s.find('.') { if let Some(idx) = s.find('.') {
let m = s[..idx].to_string(); let m = s[..idx].to_string();
@@ -517,14 +517,14 @@ fn main() -> Result<()> {
} }
}); });
// Edges sammeln: (from_module, from_def, target). // Collect edges: (from_module, from_def, target).
// `target` ist entweder `Edge::Def { to_module, to_def }` // `target` is either `Edge::Def { to_module, to_def }`
// oder `Edge::Effect(eff/op)`. // or `Edge::Effect(eff/op)`.
let mut def_edges: Vec<(String, String, String, String)> = Vec::new(); let mut def_edges: Vec<(String, String, String, String)> = Vec::new();
let mut effect_edges: Vec<(String, String, String)> = Vec::new(); let mut effect_edges: Vec<(String, String, String)> = Vec::new();
for (mod_name, m) in &ws.modules { for (mod_name, m) in &ws.modules {
// Import-Map des Moduls — nötig zur Cross-Modul-Auflösung. // Import map of the module — needed for cross-module resolution.
let import_map = build_import_map(m); let import_map = build_import_map(m);
for d in &m.defs { for d in &m.defs {
@@ -541,8 +541,8 @@ fn main() -> Result<()> {
let refs = collect_refs(d); let refs = collect_refs(d);
for r in &refs { for r in &refs {
// Effekt-Refs sind als `effect:<eff>/<op>` kodiert // Effect refs are encoded as `effect:<eff>/<op>`
// (siehe `walk_term`). // (see `walk_term`).
if let Some(rest) = r.strip_prefix("effect:") { if let Some(rest) = r.strip_prefix("effect:") {
effect_edges.push(( effect_edges.push((
mod_name.clone(), mod_name.clone(),
@@ -551,9 +551,9 @@ fn main() -> Result<()> {
)); ));
continue; continue;
} }
// ctor:* / type:* — keine Cross-Module-Defs für // ctor:* / type:* — no cross-module defs at the
// den MVP-Sprachstand; als opaker Marker mit // MVP language stage; pass through as an opaque
// leerem to_module durchreichen. // marker with empty to_module.
if r.starts_with("ctor:") || r.starts_with("type:") { if r.starts_with("ctor:") || r.starts_with("type:") {
def_edges.push(( def_edges.push((
mod_name.clone(), mod_name.clone(),
@@ -563,7 +563,7 @@ fn main() -> Result<()> {
)); ));
continue; continue;
} }
// Var-Ref: kann lokal oder qualifiziert (`pre.def`) sein. // Var ref: can be local or qualified (`pre.def`).
if let Some(idx) = r.find('.') { if let Some(idx) = r.find('.') {
let pre = &r[..idx]; let pre = &r[..idx];
let to_def = &r[idx + 1..]; let to_def = &r[idx + 1..];
@@ -578,7 +578,7 @@ fn main() -> Result<()> {
to_def.to_string(), to_def.to_string(),
)); ));
} else { } else {
// Lokale Referenzbleibt im selben Modul. // Local referencestays in the same module.
def_edges.push(( def_edges.push((
mod_name.clone(), mod_name.clone(),
d.name().to_string(), d.name().to_string(),
@@ -661,10 +661,10 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
/// Wandelt einen `WorkspaceLoadError` in ein passendes Diagnostic für den /// Converts a `WorkspaceLoadError` into a suitable diagnostic for the
/// JSON-Modus von `ail check`. Reine I/O-Fehler haben kein Modul-Diagnostic- /// JSON mode of `ail check`. Pure I/O errors have no module diagnostic
/// Äquivalent (sie sind nicht der Pipeline-Sache eines Konsumenten); für /// equivalent (they aren't the pipeline's concern for a consumer); for
/// die liefern wir `None` und lassen den Aufrufer fatal scheitern. /// those we return `None` and let the caller fail fatally.
fn workspace_error_to_diagnostic( fn workspace_error_to_diagnostic(
e: &ailang_core::WorkspaceLoadError, e: &ailang_core::WorkspaceLoadError,
) -> Option<ailang_check::Diagnostic> { ) -> Option<ailang_check::Diagnostic> {
@@ -741,7 +741,7 @@ fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
ailang_core::Def::Fn(f) => walk_term(&f.body, &mut out), ailang_core::Def::Fn(f) => walk_term(&f.body, &mut out),
ailang_core::Def::Const(c) => walk_term(&c.value, &mut out), ailang_core::Def::Const(c) => walk_term(&c.value, &mut out),
ailang_core::Def::Type(td) => { ailang_core::Def::Type(td) => {
// Eine Typedef referenziert die Typen ihrer Felder. // A type def references the types of its fields.
for c in &td.ctors { for c in &td.ctors {
for ft in &c.fields { for ft in &c.fields {
if let ailang_core::Type::Con { name } = ft { if let ailang_core::Type::Con { name } = ft {
@@ -777,8 +777,8 @@ fn walk_term(t: &ailang_core::Term, out: &mut std::collections::BTreeSet<String>
walk_term(else_, out); walk_term(else_, out);
} }
Term::Do { op, args } => { Term::Do { op, args } => {
// Effekt-Ops als `effect:io/print_int` markieren, damit man sie // Mark effect ops as `effect:io/print_int` so they can be
// von normalen Funktionsaufrufen trennen kann. // separated from normal function calls.
out.insert(format!("effect:{op}")); out.insert(format!("effect:{op}"));
for a in args { for a in args {
walk_term(a, out); walk_term(a, out);
@@ -804,8 +804,8 @@ fn walk_term(t: &ailang_core::Term, out: &mut std::collections::BTreeSet<String>
// --- ail diff ------------------------------------------------------------- // --- ail diff -------------------------------------------------------------
/// Rein struktureller Modul-Diff. Top-Level-Defs werden per `name` /// Purely structural module diff. Top-level defs are identified by
/// identifiziert und per BLAKE3-16-Hex der canonical Bytes verglichen. /// `name` and compared via the BLAKE3-16-hex of the canonical bytes.
struct DiffReport { struct DiffReport {
module_a: String, module_a: String,
module_b: String, module_b: String,
@@ -847,9 +847,9 @@ fn build_diff(a: &ailang_core::Module, b: &ailang_core::Module) -> DiffReport {
} }
} }
/// Reine Listen-Diff-Berechnung für zwei Def-Slices. Wird sowohl vom /// Pure list-diff computation for two def slices. Used both by the
/// Single-Modul-Diff als auch — pro `changed_module` — vom Workspace-Diff /// single-module diff and — per `changed_module` — by the workspace diff,
/// verwendet, damit die 4-Kategorie-Logik nur an einer Stelle lebt. /// so the 4-category logic lives in only one place.
fn diff_def_lists( fn diff_def_lists(
a_defs: &[ailang_core::Def], a_defs: &[ailang_core::Def],
b_defs: &[ailang_core::Def], b_defs: &[ailang_core::Def],
@@ -957,8 +957,8 @@ fn render_diff_text(r: &DiffReport) -> String {
return out; return out;
} }
// Einheitliche Spaltenbreite für Namens-/Kind-Spalte, damit Hashes // Uniform column width for the name/kind column so hashes line up
// visuell aligned sind. Längster Name bestimmt die Breite. // visually. The longest name sets the width.
let name_width = r let name_width = r
.added .added
.iter() .iter()
@@ -990,7 +990,7 @@ fn render_diff_text(r: &DiffReport) -> String {
); );
} }
for c in &r.changed { for c in &r.changed {
// Wenn sich der Kind geändert hat (z. B. const → fn), beide zeigen. // If the kind changed (e.g. const → fn), show both.
let kind = if c.kind_a == c.kind_b { let kind = if c.kind_a == c.kind_b {
c.kind_a.to_string() c.kind_a.to_string()
} else { } else {
@@ -1019,11 +1019,10 @@ fn render_diff_text(r: &DiffReport) -> String {
out out
} }
// --- Workspace-fähige Helfer (Iter 5d) ------------------------------------ // --- Workspace-aware helpers (Iter 5d) ------------------------------------
/// Kompakte Zusammenfassung einer Def für Manifest-Ausgaben (Single- und /// Compact summary of a def for manifest output (single and workspace
/// Workspace-Modus teilen diesen Helper). Liefert (kind, type-string, /// mode share this helper). Returns (kind, type-string, effects).
/// effects).
fn def_summary(d: &ailang_core::Def) -> (&'static str, String, Vec<String>) { fn def_summary(d: &ailang_core::Def) -> (&'static str, String, Vec<String>) {
match d { match d {
ailang_core::Def::Fn(f) => { ailang_core::Def::Fn(f) => {
@@ -1064,11 +1063,11 @@ fn def_summary(d: &ailang_core::Def) -> (&'static str, String, Vec<String>) {
} }
} }
/// Auflösung für `ail describe --workspace <name>`. /// Resolution for `ail describe --workspace <name>`.
/// ///
/// 1. `name` enthält genau einen Punkt → `<modul>.<def>` strikt auflösen. /// 1. `name` contains exactly one dot → resolve `<module>.<def>` strictly.
/// 2. Sonst zuerst im Eintrittsmodul suchen; nur fallback auf andere Module, /// 2. Otherwise search the entry module first; fall back to other modules
/// wenn dort nichts. Mehrere Treffer ergeben `ambiguous-name`. /// only if nothing there. Multiple hits produce `ambiguous-name`.
fn resolve_describe_name<'ws>( fn resolve_describe_name<'ws>(
ws: &'ws ailang_core::Workspace, ws: &'ws ailang_core::Workspace,
name: &str, name: &str,
@@ -1089,14 +1088,14 @@ fn resolve_describe_name<'ws>(
return Ok((mod_name.to_string(), def)); return Ok((mod_name.to_string(), def));
} }
// Bare-Name: erst Eintrittsmodul. // Bare name: entry module first.
if let Some(entry_mod) = ws.modules.get(&ws.entry) { if let Some(entry_mod) = ws.modules.get(&ws.entry) {
if let Some(def) = entry_mod.defs.iter().find(|d| d.name() == name) { if let Some(def) = entry_mod.defs.iter().find(|d| d.name() == name) {
return Ok((ws.entry.clone(), def)); return Ok((ws.entry.clone(), def));
} }
} }
// Fallback: alle Module einsammeln; bei Mehrdeutigkeit Fehler. // Fallback: collect all modules; error on ambiguity.
let mut hits: Vec<(String, &ailang_core::Def)> = Vec::new(); let mut hits: Vec<(String, &ailang_core::Def)> = Vec::new();
for (mod_name, m) in &ws.modules { for (mod_name, m) in &ws.modules {
if mod_name == &ws.entry { if mod_name == &ws.entry {
@@ -1125,9 +1124,9 @@ fn resolve_describe_name<'ws>(
} }
} }
/// Map `<prefix> -> <module-name>` für ein einzelnes Modul. `<prefix>` ist /// Map `<prefix> -> <module-name>` for a single module. `<prefix>` is
/// der Import-Alias falls gesetzt, sonst der Modulname selbst. Modulname /// the import alias if set, otherwise the module name itself. Module name
/// ohne Punkt. /// without a dot.
fn build_import_map(m: &ailang_core::Module) -> std::collections::BTreeMap<String, String> { fn build_import_map(m: &ailang_core::Module) -> std::collections::BTreeMap<String, String> {
let mut map = std::collections::BTreeMap::new(); let mut map = std::collections::BTreeMap::new();
for imp in &m.imports { for imp in &m.imports {
@@ -1137,7 +1136,7 @@ fn build_import_map(m: &ailang_core::Module) -> std::collections::BTreeMap<Strin
map map
} }
// --- Workspace-Diff ------------------------------------------------------- // --- Workspace diff -------------------------------------------------------
struct WorkspaceDiffReport { struct WorkspaceDiffReport {
workspace_a: String, workspace_a: String,
+39 -39
View File
@@ -1,7 +1,7 @@
//! End-to-End-Test: lade Beispielmodul, kompiliere zu Binary, führe es aus. //! End-to-end test: load example module, compile to binary, run it.
//! //!
//! Dieses Test schützt die wichtigste Eigenschaft der gesamten Pipeline: //! This test guards the most important property of the entire pipeline:
//! AST → typecheck → LLVM IR → clang → Binary → korrekter stdout. //! AST → typecheck → LLVM IR → clang → binary → correct stdout.
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
@@ -11,7 +11,7 @@ fn ail_bin() -> &'static str {
} }
fn build_and_run(example: &str) -> String { fn build_and_run(example: &str) -> String {
// Workspace-Root liegt zwei Ebenen über dem Crate-Manifest. // Workspace root is two levels above the crate manifest.
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src = workspace.join("examples").join(example); let src = workspace.join("examples").join(example);
@@ -43,8 +43,8 @@ fn sum_1_to_10_is_55() {
assert_eq!(stdout.trim(), "55"); assert_eq!(stdout.trim(), "55");
} }
/// Schützt das Block-Tracking im Codegen: max3 hat verschachtelte `if`s, /// Guards block tracking in codegen: max3 has nested `if`s, and wrong
/// und falsches phi-Block-Tracking würde hier zu falschem Ergebnis führen. /// phi block tracking would produce a wrong result here.
#[test] #[test]
fn max3_picks_largest() { fn max3_picks_largest() {
let stdout = build_and_run("max3.ail.json"); let stdout = build_and_run("max3.ail.json");
@@ -57,24 +57,24 @@ fn hello_world_str_lit() {
assert_eq!(stdout.trim(), "Hallo, AILang."); assert_eq!(stdout.trim(), "Hallo, AILang.");
} }
/// Schützt ADT-Codegen + Match: rekursive Liste, sum_list via match auf Cons/Nil. /// Guards ADT codegen + match: recursive list, sum_list via match on Cons/Nil.
#[test] #[test]
fn list_sum_via_match() { fn list_sum_via_match() {
let stdout = build_and_run("list.ail.json"); let stdout = build_and_run("list.ail.json");
assert_eq!(stdout.trim(), "42"); assert_eq!(stdout.trim(), "42");
} }
/// Schützt `ail diff`: ein modifizierter Body ändert den Hash von `sum`, /// Guards `ail diff`: a modified body changes the hash of `sum`, while
/// während `main` unverändert bleibt. Erwartet Exit-Code 1, `changed` /// `main` stays unchanged. Expects exit code 1, `changed` contains exactly
/// enthält genau `sum`, `unchanged` enthält `main`, `added`/`removed` leer. /// `sum`, `unchanged` contains `main`, `added`/`removed` empty.
#[test] #[test]
fn diff_detects_changed_def() { fn diff_detects_changed_def() {
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src_a = workspace.join("examples").join("sum.ail.json"); let src_a = workspace.join("examples").join("sum.ail.json");
// Variante: lade sum.ail.json, mutiere den `then`-Zweig (statt 0 nun 1) // Variant: load sum.ail.json, mutate the `then` branch (1 instead of 0)
// in der `sum`-Definition. `main` bleibt bitidentisch. // in the `sum` definition. `main` stays bit-identical.
let raw = std::fs::read(&src_a).expect("read sum.ail.json"); let raw = std::fs::read(&src_a).expect("read sum.ail.json");
let mut module: serde_json::Value = serde_json::from_slice(&raw).expect("parse sum.ail.json"); let mut module: serde_json::Value = serde_json::from_slice(&raw).expect("parse sum.ail.json");
{ {
@@ -84,7 +84,7 @@ fn diff_detects_changed_def() {
.expect("defs array"); .expect("defs array");
for def in defs.iter_mut() { for def in defs.iter_mut() {
if def.get("name").and_then(|n| n.as_str()) == Some("sum") { if def.get("name").and_then(|n| n.as_str()) == Some("sum") {
// Ersetze den then-Zweig literal 0 → literal 1. // Replace the then branch literal 0 → literal 1.
let new_then = serde_json::json!({ let new_then = serde_json::json!({
"t": "lit", "t": "lit",
"lit": { "kind": "int", "value": 1 } "lit": { "kind": "int", "value": 1 }
@@ -149,7 +149,7 @@ fn diff_detects_changed_def() {
); );
} }
/// Diff eines Moduls mit sich selbst: Exit 0, alle Listen außer `unchanged` leer. /// Diff of a module with itself: exit 0, all lists except `unchanged` empty.
#[test] #[test]
fn diff_no_changes_exit_zero() { fn diff_no_changes_exit_zero() {
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -187,10 +187,10 @@ fn diff_no_changes_exit_zero() {
); );
} }
/// Schützt den Workspace-Loader (Iter 5a): das Eintrittsmodul `ws_main` /// Guards the workspace loader (Iter 5a): the entry module `ws_main`
/// importiert `ws_lib`, beide müssen vom Loader gefunden, geladen und im /// imports `ws_lib`; both must be found by the loader, loaded, and listed
/// JSON-Output aufgelistet sein. Cross-Module-Typcheck/Codegen ist explizit /// in the JSON output. Cross-module typecheck/codegen is explicitly not
/// nicht Teil dieses Testser verifiziert nur die Lader-Pipeline. /// part of this test — it only verifies the loader pipeline.
#[test] #[test]
fn workspace_lists_imported_modules() { fn workspace_lists_imported_modules() {
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -229,9 +229,9 @@ fn workspace_lists_imported_modules() {
assert!(names.contains(&"ws_lib"), "ws_lib missing: {names:?}"); assert!(names.contains(&"ws_lib"), "ws_lib missing: {names:?}");
} }
/// Schützt Iter 5b: `ail check examples/ws_main.ail.json --json` muss den /// Guards Iter 5b: `ail check examples/ws_main.ail.json --json` must
/// Cross-Module-Aufruf `ws_lib.add` auflösen können. Erwartet: Exit 0, /// resolve the cross-module call `ws_lib.add`. Expected: exit 0,
/// stdout exakt `[]` (leeres Diagnostic-Array). /// stdout exactly `[]` (empty diagnostic array).
#[test] #[test]
fn check_workspace_resolves_import() { fn check_workspace_resolves_import() {
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -256,17 +256,17 @@ fn check_workspace_resolves_import() {
assert_eq!(stdout.trim(), "[]", "expected empty diagnostic array"); assert_eq!(stdout.trim(), "[]", "expected empty diagnostic array");
} }
/// Schützt Iter 5c (Cross-Module-Codegen): `ail build` über /// Guards Iter 5c (cross-module codegen): `ail build` over
/// `examples/ws_main.ail.json` muss das Workspace inkl. `ws_lib` lowern, /// `examples/ws_main.ail.json` must lower the workspace incl. `ws_lib`;
/// `@ail_ws_main_main` muss `@ail_ws_lib_add(2,3)` aufrufen und `5` drucken. /// `@ail_ws_main_main` must call `@ail_ws_lib_add(2,3)` and print `5`.
#[test] #[test]
fn workspace_build_runs_imported_fn() { fn workspace_build_runs_imported_fn() {
let stdout = build_and_run("ws_main.ail.json"); let stdout = build_and_run("ws_main.ail.json");
assert_eq!(stdout.trim(), "5", "ws_lib.add(2,3) should print 5"); assert_eq!(stdout.trim(), "5", "ws_lib.add(2,3) should print 5");
} }
/// Schützt Iter 5d: `ail manifest --workspace --json` listet Defs aus allen /// Guards Iter 5d: `ail manifest --workspace --json` lists defs from all
/// Modulen des Workspacessichtbar an einem `module`-Feld pro Eintrag. /// modules of the workspace — visible via a `module` field per entry.
#[test] #[test]
fn manifest_workspace_lists_all_defs() { fn manifest_workspace_lists_all_defs() {
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -308,8 +308,8 @@ fn manifest_workspace_lists_all_defs() {
); );
} }
/// Schützt Iter 5d: `ail describe --workspace ws_lib.add` löst die /// Guards Iter 5d: `ail describe --workspace ws_lib.add` resolves the
/// qualifizierte Punkt-Notation in das tatsächlich importierte Modul auf. /// qualified dot notation to the actually imported module.
#[test] #[test]
fn describe_workspace_resolves_qualified_name() { fn describe_workspace_resolves_qualified_name() {
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -349,8 +349,8 @@ fn describe_workspace_resolves_qualified_name() {
); );
} }
/// Schützt Iter 5d: `ail deps --workspace` enthält eine Cross-Module-Edge /// Guards Iter 5d: `ail deps --workspace` contains a cross-module edge
/// `ws_main.main -> ws_lib.add`, weil `ws_main` `ws_lib.add(2,3)` aufruft. /// `ws_main.main -> ws_lib.add`, because `ws_main` calls `ws_lib.add(2,3)`.
#[test] #[test]
fn deps_workspace_includes_cross_module() { fn deps_workspace_includes_cross_module() {
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -385,8 +385,8 @@ fn deps_workspace_includes_cross_module() {
); );
} }
/// Schützt Iter 5d: `ail diff --workspace` erkennt ein zusätzliches Modul /// Guards Iter 5d: `ail diff --workspace` detects an additional module
/// im B-Workspace als `added_modules`-Eintrag und exitet mit Code 1. /// in the B workspace as an `added_modules` entry and exits with code 1.
#[test] #[test]
fn diff_workspace_added_module() { fn diff_workspace_added_module() {
use std::fs; use std::fs;
@@ -409,9 +409,9 @@ fn diff_workspace_added_module() {
fs::create_dir_all(&dir_a).unwrap(); fs::create_dir_all(&dir_a).unwrap();
fs::create_dir_all(&dir_b).unwrap(); fs::create_dir_all(&dir_b).unwrap();
// Beide Workspaces haben ein leeres Modul `root`. B importiert // Both workspaces have an empty module `root`. B additionally
// zusätzlich `extra`. Loader-Konvention: `<root_dir>/<name>.ail.json`, // imports `extra`. Loader convention: `<root_dir>/<name>.ail.json`,
// Modul-Name muss zum Dateinamen passen. // module name must match the file name.
let empty_root = serde_json::json!({ let empty_root = serde_json::json!({
"schema": "ailang/v0", "schema": "ailang/v0",
"name": "root", "name": "root",
@@ -477,10 +477,10 @@ fn diff_workspace_added_module() {
); );
} }
/// Schützt das `--json`-Diagnostic-Format für Tooling-Konsumenten. /// Guards the `--json` diagnostic format for tooling consumers.
/// `broken_unbound.ail.json` referenziert eine nicht-existente Variable; /// `broken_unbound.ail.json` references a non-existent variable;
/// erwartet wird Exit-Code 1 und mindestens ein Diagnostic mit /// exit code 1 is expected, plus at least one diagnostic with
/// `severity == "error"` und `code == "unbound-var"`. /// `severity == "error"` and `code == "unbound-var"`.
#[test] #[test]
fn check_json_unbound_var() { fn check_json_unbound_var() {
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
+13 -13
View File
@@ -1,5 +1,5 @@
//! IR-Snapshot-Tests: schützen die Codegen-Pipeline vor unbeabsichtigten //! IR snapshot tests: guard the codegen pipeline against unintended
//! Veränderungen am LLVM-IR. Snapshots in tests/snapshots/. Update mit //! changes to the LLVM IR. Snapshots in tests/snapshots/. Update with
//! UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_. //! UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_.
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -9,17 +9,17 @@ fn ail_bin() -> &'static str {
env!("CARGO_BIN_EXE_ail") env!("CARGO_BIN_EXE_ail")
} }
/// Normalisiert IR-Text für Plattform-Stabilität: /// Normalizes IR text for platform stability:
/// - `target triple = "..."` -> `target triple = "<NORMALIZED>"` /// - `target triple = "..."` -> `target triple = "<NORMALIZED>"`
/// - trailing whitespace pro Zeile entfernen /// - strip trailing whitespace per line
/// - LF-Zeilenenden, Datei endet mit genau einem `\n` /// - LF line endings, file ends with exactly one `\n`
fn normalize(ir: &str) -> String { fn normalize(ir: &str) -> String {
let mut out = String::with_capacity(ir.len()); let mut out = String::with_capacity(ir.len());
for line in ir.split('\n') { for line in ir.split('\n') {
let line = line.strip_suffix('\r').unwrap_or(line); let line = line.strip_suffix('\r').unwrap_or(line);
let trimmed = line.trim_end(); let trimmed = line.trim_end();
if let Some(rest) = trimmed.strip_prefix("target triple = ") { if let Some(rest) = trimmed.strip_prefix("target triple = ") {
// rest ist `"..."` (mit Anführungszeichen) — komplett normalisieren. // rest is `"..."` (with quotes) — fully normalize.
let _ = rest; let _ = rest;
out.push_str("target triple = \"<NORMALIZED>\""); out.push_str("target triple = \"<NORMALIZED>\"");
} else { } else {
@@ -27,9 +27,9 @@ fn normalize(ir: &str) -> String {
} }
out.push('\n'); out.push('\n');
} }
// split('\n') erzeugt nach einem trailing `\n` ein leeres letztes Stück, // split('\n') after a trailing `\n` produces an empty last piece,
// das oben mit `\n` abgeschlossen wurde. Dadurch enthält `out` typischerweise // which was closed with `\n` above. So `out` typically ends in
// genau ein abschließendes `\n`. Doppelte `\n\n` am Ende reduzieren. // exactly one trailing `\n`. Reduce double `\n\n` at the end.
while out.ends_with("\n\n") { while out.ends_with("\n\n") {
out.pop(); out.pop();
} }
@@ -152,10 +152,10 @@ fn ir_snapshot_list() {
check_ir_snapshot("list.ail.json", "list.ll"); check_ir_snapshot("list.ail.json", "list.ll");
} }
/// Schützt das Workspace-Lowering (Iter 5c): das Eintrittsmodul `ws_main` /// Guards workspace lowering (Iter 5c): the entry module `ws_main`
/// importiert `ws_lib`, beide werden in derselben `.ll` emittiert, /// imports `ws_lib`; both are emitted into the same `.ll`,
/// `@ail_ws_main_main` ruft `@ail_ws_lib_add` auf, und das Trampoline /// `@ail_ws_main_main` calls `@ail_ws_lib_add`, and the trampoline
/// `@main` ruft `@ail_ws_main_main`. /// `@main` calls `@ail_ws_main_main`.
#[test] #[test]
fn ir_snapshot_ws_main() { fn ir_snapshot_ws_main() {
check_ir_snapshot("ws_main.ail.json", "ws_main.ll"); check_ir_snapshot("ws_main.ail.json", "ws_main.ll");
+3 -3
View File
@@ -1,4 +1,4 @@
//! Built-in Operationen, die der Typchecker (und Codegen) kennen. //! Built-in operations known to the typechecker (and codegen).
use ailang_core::ast::Type; use ailang_core::ast::Type;
@@ -61,8 +61,8 @@ pub fn install(env: &mut crate::Env) {
); );
} }
/// Liefert die Liste aller registrierten Built-ins. Praktisch für CLI-Subcommand /// Returns the list of all registered built-ins. Useful for the CLI subcommand
/// `ail builtins`, wenn der LLM erwartete Signaturen prüfen will. /// `ail builtins`, when the LLM wants to check expected signatures.
pub fn list() -> Vec<(&'static str, &'static str)> { pub fn list() -> Vec<(&'static str, &'static str)> {
vec![ vec![
("+", "(Int, Int) -> Int"), ("+", "(Int, Int) -> Int"),
+20 -20
View File
@@ -1,16 +1,16 @@
//! Strukturierte Diagnostics für `ail check --json`. //! Structured diagnostics for `ail check --json`.
//! //!
//! Eine [`Diagnostic`] ist die maschinenlesbare Repräsentation eines vom //! A [`Diagnostic`] is the machine-readable representation of a problem
//! Typchecker (oder vorgelagertem Lade-Schritt) gemeldeten Problems. //! reported by the typechecker (or an upstream load step).
//! Die [`Severity`] serialisiert sich als kleingeschriebener String //! [`Severity`] serializes as a lowercase string
//! (`"error"` / `"warning"`), [`code`] ist ein stabiler Kebab-Case-Identifier, //! (`"error"` / `"warning"`); [`code`] is a stable kebab-case identifier
//! der von Tooling konsumiert werden kann, ohne den `message`-Text zu parsen. //! that tooling can consume without parsing the `message` text.
//! //!
//! Konvention: pro Aufruf von [`super::check_module`] wird höchstens ein //! Convention: each call to [`super::check_module`] reports at most one
//! Diagnostic gemeldet (single-shot). Mehrere Diagnostics pro Lauf sind ein //! diagnostic (single-shot). Multiple diagnostics per run are a future
//! späteres Feature; das aktuelle Format erlaubt sie aber bereits. //! feature; the current format already allows them.
//! //!
//! Stabile Codes (Stand Iteration 5b): //! Stable codes (as of iteration 5b):
//! - `schema-mismatch` //! - `schema-mismatch`
//! - `unknown-type` //! - `unknown-type`
//! - `unbound-var` //! - `unbound-var`
@@ -35,10 +35,10 @@
//! - `unknown-module` — `ctx`: `{"module": "<prefix>"}` (Iter 5b) //! - `unknown-module` — `ctx`: `{"module": "<prefix>"}` (Iter 5b)
//! - `unknown-import` — `ctx`: `{"module": "<m>", "name": "<def>"}` (Iter 5b) //! - `unknown-import` — `ctx`: `{"module": "<m>", "name": "<def>"}` (Iter 5b)
//! - `invalid-def-name` — `ctx`: `{"name": "<n>", "reason": "contains-dot"}` (Iter 5b) //! - `invalid-def-name` — `ctx`: `{"name": "<n>", "reason": "contains-dot"}` (Iter 5b)
//! - `module-not-found` — Workspace-Loader (Iter 5b, im CLI-Pfad) //! - `module-not-found` — workspace loader (Iter 5b, in the CLI path)
//! - `module-cycle` — Workspace-Loader (Iter 5b, im CLI-Pfad) //! - `module-cycle` — workspace loader (Iter 5b, in the CLI path)
//! - `module-name-mismatch` — Workspace-Loader (Iter 5b, im CLI-Pfad) //! - `module-name-mismatch` — workspace loader (Iter 5b, in the CLI path)
//! - `module-hash-mismatch` — Workspace-Loader (Iter 5b, im CLI-Pfad) //! - `module-hash-mismatch` — workspace loader (Iter 5b, in the CLI path)
use serde::Serialize; use serde::Serialize;
@@ -54,11 +54,11 @@ pub struct Diagnostic {
pub severity: Severity, pub severity: Severity,
pub code: String, pub code: String,
pub message: String, pub message: String,
/// Welche Top-Level-Definition betroffen ist (falls bekannt). Wird im /// Which top-level definition is affected (if known). Always emitted in
/// JSON immer ausgegeben — `null`, wenn unbekannt — damit Konsumenten /// the JSON — `null` when unknown — so consumers don't have to handle
/// das Feld nicht konditionell behandeln müssen. /// the field conditionally.
pub def: Option<String>, pub def: Option<String>,
/// Freier strukturierter Kontext. Leer = `{}`. /// Free structured context. Empty = `{}`.
pub ctx: serde_json::Value, pub ctx: serde_json::Value,
} }
@@ -93,8 +93,8 @@ mod tests {
let d = Diagnostic::error("unbound-var", "unknown identifier: `x`") let d = Diagnostic::error("unbound-var", "unknown identifier: `x`")
.with_def("main"); .with_def("main");
let s = serde_json::to_string(&d).unwrap(); let s = serde_json::to_string(&d).unwrap();
// Felder müssen alle vorhanden sein, Severity klein, ctx ist ein // All fields must be present, severity lowercase, ctx is an
// leeres Objekt (nicht null, nicht weggelassen). // empty object (not null, not omitted).
assert!(s.contains("\"severity\":\"error\""), "{s}"); assert!(s.contains("\"severity\":\"error\""), "{s}");
assert!(s.contains("\"code\":\"unbound-var\""), "{s}"); assert!(s.contains("\"code\":\"unbound-var\""), "{s}");
assert!(s.contains("\"def\":\"main\""), "{s}"); assert!(s.contains("\"def\":\"main\""), "{s}");
+106 -105
View File
@@ -1,10 +1,10 @@
//! Typchecker für AILang (MVP). //! Typechecker for AILang (MVP).
//! //!
//! Monomorpher HM-Subset: keine Type-Variablen im Body, alle Top-Level-Defs //! Monomorphic HM subset: no type variables in the body; all top-level defs
//! müssen vollständig annotiert sein. Effekte werden als Set propagiert //! must be fully annotated. Effects are propagated as a set and reconciled
//! und mit der Annotation am Funktionstyp abgeglichen. //! against the annotation on the function type.
//! //!
//! Eingebaute Operationen werden über [`Builtins`] aufgelöst. //! Built-in operations are resolved via [`Builtins`].
use ailang_core::ast::*; use ailang_core::ast::*;
use ailang_core::Workspace; use ailang_core::Workspace;
@@ -110,9 +110,9 @@ pub enum CheckError {
type Result<T> = std::result::Result<T, CheckError>; type Result<T> = std::result::Result<T, CheckError>;
impl CheckError { impl CheckError {
/// Stabiler Kebab-Case-Code für maschinelle Konsumption (`ail check --json`). /// Stable kebab-case code for machine consumption (`ail check --json`).
/// Wird über das `Def`-Wrapping rekursiv durchgereicht — der innere Fehler /// Passed through recursively via the `Def` wrapping — the inner error
/// trägt den eigentlichen Code, das Wrapping nur den Def-Kontext. /// carries the actual code, the wrapper only the def context.
pub fn code(&self) -> &'static str { pub fn code(&self) -> &'static str {
match self { match self {
CheckError::Def(_, inner) => inner.code(), CheckError::Def(_, inner) => inner.code(),
@@ -143,8 +143,8 @@ impl CheckError {
} }
} }
/// Strukturierter Kontext für ein Diagnostic. Kommt direkt im JSON unter /// Structured context for a diagnostic. Lands directly in the JSON
/// dem Schlüssel `ctx` an. Leeres Objekt, wenn kein Kontext vorhanden. /// under the key `ctx`. Empty object when no context is available.
pub fn ctx(&self) -> serde_json::Value { pub fn ctx(&self) -> serde_json::Value {
match self { match self {
CheckError::Def(_, inner) => inner.ctx(), CheckError::Def(_, inner) => inner.ctx(),
@@ -178,8 +178,8 @@ impl CheckError {
} }
} }
/// Falls dieser Fehler durch [`CheckError::Def`] gewrappt ist, liefert /// If this error is wrapped by [`CheckError::Def`], returns the name
/// diese Methode den Namen der betroffenen Def. Sonst `None`. /// of the affected def. Otherwise `None`.
pub fn def(&self) -> Option<&str> { pub fn def(&self) -> Option<&str> {
match self { match self {
CheckError::Def(n, _) => Some(n.as_str()), CheckError::Def(n, _) => Some(n.as_str()),
@@ -187,7 +187,7 @@ impl CheckError {
} }
} }
/// Auspacken des potenziell durch [`CheckError::Def`] gewrappten Fehlers. /// Unwraps the error potentially wrapped by [`CheckError::Def`].
pub fn inner(&self) -> &CheckError { pub fn inner(&self) -> &CheckError {
match self { match self {
CheckError::Def(_, inner) => inner.inner(), CheckError::Def(_, inner) => inner.inner(),
@@ -195,7 +195,7 @@ impl CheckError {
} }
} }
/// Nicht-`Def`-gewrapptes Message. Ohne `def: ...`-Präfix. /// Non-`Def`-wrapped message. Without the `def: ...` prefix.
pub fn message(&self) -> String { pub fn message(&self) -> String {
format!("{}", self.inner()) format!("{}", self.inner())
} }
@@ -209,18 +209,18 @@ impl CheckError {
} }
} }
/// Top-Level-API für strukturierte Diagnostics. /// Top-level API for structured diagnostics.
/// ///
/// Leerer Vec = grün. Im aktuellen Stand wird beim ersten Fehler abgebrochen, /// Empty Vec = green. In the current state, processing aborts on the first
/// daher enthält der Vec entweder 0 oder 1 Element. Mehrere Diagnostics pro /// error, so the Vec contains either 0 or 1 element. Multiple diagnostics
/// Lauf sind ein späteres Feature; das Format erlaubt sie bereits. /// per run are a future feature; the format already allows them.
/// ///
/// ckwärtskompatibilität: ein nackter `&Module` wird intern in einen /// Backwards compatibility: a bare `&Module` is internally lifted into a
/// Trivial-Workspace gehoben (`modules = {m.name: m}`, `entry = m.name`), /// trivial workspace (`modules = {m.name: m}`, `entry = m.name`) so that
/// damit Tooling, das einzelne Module checkt, ohne `Workspace`-Bau auskommt. /// tooling checking individual modules avoids building a `Workspace`.
/// Module mit Imports auf nicht im Trivial-Workspace vorhandene andere /// Modules with imports on other modules not present in the trivial
/// Module werden hier zwangsläufig `unknown-module`-Fehler bei qualifizierten /// workspace will inevitably produce `unknown-module` errors on qualified
/// Referenzen liefern — was korrekt ist. /// references — which is correct.
pub fn check_module(m: &Module) -> Vec<Diagnostic> { pub fn check_module(m: &Module) -> Vec<Diagnostic> {
let mut modules = BTreeMap::new(); let mut modules = BTreeMap::new();
modules.insert(m.name.clone(), m.clone()); modules.insert(m.name.clone(), m.clone());
@@ -232,33 +232,33 @@ pub fn check_module(m: &Module) -> Vec<Diagnostic> {
check_workspace(&ws) check_workspace(&ws)
} }
/// Top-Level-API für Cross-Module-Typcheck. /// Top-level API for cross-module typecheck.
/// ///
/// Iteriert über alle Module des Workspaces und prüft jedes mit Zugriff auf /// Iterates over all modules of the workspace and checks each with access
/// die Top-Level-Symboltabellen aller anderen Module. Qualifizierte /// to the top-level symbol tables of all other modules. Qualified
/// Referenzen werden über die Import-Map des jeweiligen Moduls aufgelöst: /// references are resolved via the import map of the respective module:
/// `Term::Var { name }` mit genau einem Punkt im Namen wird als /// `Term::Var { name }` with exactly one dot in the name is interpreted
/// `<prefix>.<def>` interpretiert; `<prefix>` ist ein Import-Alias (oder /// as `<prefix>.<def>`; `<prefix>` is an import alias (or the module name,
/// der Modulname, falls ohne Alias importiert). /// if imported without an alias).
/// ///
/// Wie bei `check_module`: pro Lauf maximal **ein** Diagnostic /// As with `check_module`: at most **one** diagnostic per run
/// (single-shot). Multi-Diagnose ist späteres Feature. /// (single-shot). Multi-diagnostic is a future feature.
pub fn check_workspace(ws: &Workspace) -> Vec<Diagnostic> { pub fn check_workspace(ws: &Workspace) -> Vec<Diagnostic> {
// Pass 1: pro Modul Top-Level-Symboltabelle aufbauen — ohne Bodies zu // Pass 1: build per-module top-level symbol table — without checking
// checken. Damit kann Modul A auf Defs aus Modul B zugreifen, auch // bodies. This lets module A access defs from module B even when B
// wenn B in der BTreeMap später kommt. Doppelte Def-Namen und // comes later in the BTreeMap. Duplicate def names and dot-in-def
// Punkt-im-Def-Namen werden hier sofort gemeldet, weil ohne saubere // names are reported here immediately, because without clean symbol
// Symboltabellen alle weiteren Diagnostics unzuverlässig wären. // tables all further diagnostics would be unreliable.
let module_globals = match build_module_globals(ws) { let module_globals = match build_module_globals(ws) {
Ok(g) => g, Ok(g) => g,
Err(e) => return vec![e.to_diagnostic()], Err(e) => return vec![e.to_diagnostic()],
}; };
// Pass 2: pro Modul body-checken. `check_in_workspace` baut den Env // Pass 2: body-check per module. `check_in_workspace` builds the env
// mit zusätzlichen Cross-Module-Globals und einer Import-Map auf. // with additional cross-module globals and an import map.
// Iterationsreihenfolge: erst das Eintrittsmodul, dann der Rest in // Iteration order: entry module first, then the rest in
// BTreeMap-Order. Damit ist die erste gemeldete Diagnostik bei // BTreeMap order. This makes the first reported diagnostic for
// Workspaces deterministisch und nahe am Entry. // workspaces deterministic and close to the entry.
let mut order: Vec<&String> = Vec::new(); let mut order: Vec<&String> = Vec::new();
if ws.modules.contains_key(&ws.entry) { if ws.modules.contains_key(&ws.entry) {
order.push(&ws.entry); order.push(&ws.entry);
@@ -277,15 +277,15 @@ pub fn check_workspace(ws: &Workspace) -> Vec<Diagnostic> {
Vec::new() Vec::new()
} }
/// Ergebnis der Typprüfung eines Moduls: Mapping vom Symbolnamen zum /// Result of typechecking a module: mapping from symbol name to
/// (Typ, Hash) — bereit für `manifest`-Ausgabe. /// (type, hash) — ready for `manifest` output.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CheckedModule { pub struct CheckedModule {
pub symbols: IndexMap<String, (Type, String)>, pub symbols: IndexMap<String, (Type, String)>,
} }
pub fn check(m: &Module) -> Result<CheckedModule> { pub fn check(m: &Module) -> Result<CheckedModule> {
// Trivial-Workspace: das Modul allein, ohne Cross-Module-Auflösung. // Trivial workspace: the module alone, without cross-module resolution.
let mut modules = BTreeMap::new(); let mut modules = BTreeMap::new();
modules.insert(m.name.clone(), m.clone()); modules.insert(m.name.clone(), m.clone());
let ws = Workspace { let ws = Workspace {
@@ -295,7 +295,7 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
}; };
let module_globals = build_module_globals(&ws)?; let module_globals = build_module_globals(&ws)?;
check_in_workspace(m, &ws, &module_globals)?; check_in_workspace(m, &ws, &module_globals)?;
// Symbole für die Rückgabe sammeln (bestehende Semantik). // Collect symbols for the return value (existing semantics).
let mut symbols = IndexMap::new(); let mut symbols = IndexMap::new();
for def in &m.defs { for def in &m.defs {
let h = ailang_core::hash::def_hash(def); let h = ailang_core::hash::def_hash(def);
@@ -311,9 +311,9 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
Ok(CheckedModule { symbols }) Ok(CheckedModule { symbols })
} }
/// Baut pro Modul die Top-Level-Symboltabelle (für Cross-Module-Lookup), /// Builds the top-level symbol table per module (for cross-module lookup),
/// ohne Bodies zu prüfen. Dupes und Punkt-im-Def-Namen werden hier sofort /// without checking bodies. Duplicates and dot-in-def names are reported
/// als Fehler gemeldet — sie würden alle weiteren Diagnostics verfälschen. /// here as errors immediately — they would taint all further diagnostics.
fn build_module_globals( fn build_module_globals(
ws: &Workspace, ws: &Workspace,
) -> Result<BTreeMap<String, IndexMap<String, Type>>> { ) -> Result<BTreeMap<String, IndexMap<String, Type>>> {
@@ -350,10 +350,10 @@ fn build_module_globals(
Ok(out) Ok(out)
} }
/// Prüft die Bodies eines einzelnen Moduls im Kontext des Workspaces. /// Checks the bodies of a single module in the context of the workspace.
/// Annahme: `module_globals` enthält bereits für **alle** Module des /// Assumption: `module_globals` already contains the top-level symbol
/// Workspaces (inklusive `m`) die Top-Level-Symboltabellen — gebaut von /// tables for **all** modules of the workspace (including `m`) — built
/// `build_module_globals`. /// by `build_module_globals`.
fn check_in_workspace( fn check_in_workspace(
m: &Module, m: &Module,
ws: &Workspace, ws: &Workspace,
@@ -362,8 +362,8 @@ fn check_in_workspace(
let mut env = Env::new(); let mut env = Env::new();
builtins::install(&mut env); builtins::install(&mut env);
// Type-Defs registrieren (lokal pro Modul; ADT-Cross-Module-Sharing ist // Register type defs (local per module; cross-module ADT sharing is
// explizit nicht Teil von 5b). // explicitly not part of 5b).
for def in &m.defs { for def in &m.defs {
if let Def::Type(td) = def { if let Def::Type(td) = def {
if env.types.contains_key(&td.name) { if env.types.contains_key(&td.name) {
@@ -394,18 +394,19 @@ fn check_in_workspace(
} }
} }
// Lokale Globals aus der vorgängig gebauten Tabelle übernehmen. // Take local globals from the previously built table.
if let Some(g) = module_globals.get(&m.name) { if let Some(g) = module_globals.get(&m.name) {
for (n, t) in g { for (n, t) in g {
env.globals.insert(n.clone(), t.clone()); env.globals.insert(n.clone(), t.clone());
} }
} }
// Import-Map aufbauen: Alias (oder Modulname, wenn ohne Alias) → // Build import map: alias (or module name, if without alias) →
// Modulname. Konflikte sind im MVP unzulässig: dieselbe `as`-Klausel // module name. Conflicts are not allowed in the MVP: the same `as`
// zweimal wäre auffällig und sollte als doppelter Symbolname auffallen // clause twice would stand out and should surface as a duplicate
// — aktuell „last wins", weil Iter 5b kein eigenes Diagnostic dafür // symbol name — currently "last wins", because Iter 5b doesn't
// einführt; falls künftig benötigt → `ambiguous-import`-Code. // introduce a dedicated diagnostic for it; if needed later →
// `ambiguous-import` code.
let mut import_map: BTreeMap<String, String> = BTreeMap::new(); let mut import_map: BTreeMap<String, String> = BTreeMap::new();
for imp in &m.imports { for imp in &m.imports {
let key = imp.alias.clone().unwrap_or_else(|| imp.module.clone()); let key = imp.alias.clone().unwrap_or_else(|| imp.module.clone());
@@ -414,9 +415,9 @@ fn check_in_workspace(
env.imports = import_map; env.imports = import_map;
env.module_globals = module_globals.clone(); env.module_globals = module_globals.clone();
env.current_module = m.name.clone(); env.current_module = m.name.clone();
// Workspace ist im Env nicht direkt nötig; Cross-Module-Lookup nutzt // Workspace isn't directly needed in the env; cross-module lookup uses
// ausschließlich `module_globals`. Aber wir behalten ws-Referenz im // only `module_globals`. But we keep the ws reference in the
// Kommentar als Erinnerung, falls künftig ADT-Cross-Module dazukommt. // comment as a reminder, in case cross-module ADTs are added later.
let _ = ws; let _ = ws;
for def in &m.defs { for def in &m.defs {
@@ -434,8 +435,8 @@ fn check_def(def: &Def, env: &Env) -> Result<()> {
} }
fn check_type_def(td: &TypeDef, env: &Env) -> Result<()> { fn check_type_def(td: &TypeDef, env: &Env) -> Result<()> {
// Felder müssen alle bekannte Typen referenzieren (oder andere ADTs aus // All fields must reference known types (or other ADTs from this
// diesem Modul; rekursiv ist erlaubt). // module; recursion is allowed).
for c in &td.ctors { for c in &td.ctors {
for f in &c.fields { for f in &c.fields {
check_type_well_formed(f, env)?; check_type_well_formed(f, env)?;
@@ -462,7 +463,7 @@ fn check_type_well_formed(t: &Type, env: &Env) -> Result<()> {
check_type_well_formed(ret, env) check_type_well_formed(ret, env)
} }
Type::Var { .. } | Type::Forall { .. } => { Type::Var { .. } | Type::Forall { .. } => {
// Im MVP keine Polymorphie auf Typebene innerhalb von ADT-Feldern. // No type-level polymorphism inside ADT fields in the MVP.
Err(CheckError::PolymorphicNotSupported( Err(CheckError::PolymorphicNotSupported(
"type def".into(), "type def".into(),
)) ))
@@ -537,38 +538,38 @@ fn synth(
Literal::Unit => Type::unit(), Literal::Unit => Type::unit(),
}), }),
Term::Var { name } => { Term::Var { name } => {
// 1) Locals haben höchste Prioritätsie können sogar einen // 1) Locals have highest prioritythey can even shadow a
// qualifizierten Punkt-Namen schatten, falls jemand einen // qualified dotted name, if someone builds a
// Buchstaben-mit-Punkt-Param baut. Das ist im MVP nicht // letter-with-dot param. Not really reachable in the MVP,
// wirklich erreichbar, aber harmlos. // but harmless.
if let Some(t) = locals.get(name) { if let Some(t) = locals.get(name) {
return Ok(t.clone()); return Ok(t.clone());
} }
// 2) Lokale Globals. // 2) Local globals.
if let Some(t) = env.globals.get(name) { if let Some(t) = env.globals.get(name) {
return Ok(t.clone()); return Ok(t.clone());
} }
// 3) Genau ein Punkt → qualifizierter Cross-Module-Verweis. // 3) Exactly one dot → qualified cross-module reference.
// Mehr als ein Punkt ist im aktuellen MVP nicht definiert // More than one dot is undefined in the current MVP
// und fällt unten als `unbound-var` durch. // and falls through below as `unbound-var`.
if name.matches('.').count() == 1 { if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked"); let (prefix, suffix) = name.split_once('.').expect("checked");
let target_module = match env.imports.get(prefix) { let target_module = match env.imports.get(prefix) {
Some(m) => m.clone(), Some(m) => m.clone(),
None => { None => {
// Selbst-Referenz `<self>.def` ohne Import-Eintrag: // Self-reference `<self>.def` without an import entry:
// erlauben wir bewusst nicht — Konvention ist, dass // we deliberately disallow this — by convention,
// qualifizierte Verweise nur über Imports gehen. // qualified references only go through imports.
// Damit bleibt die Bedeutung von `name` lokal stabil. // That way the meaning of `name` stays locally stable.
return Err(CheckError::UnknownModule { return Err(CheckError::UnknownModule {
module: prefix.to_string(), module: prefix.to_string(),
}); });
} }
}; };
let g = env.module_globals.get(&target_module).ok_or_else(|| { let g = env.module_globals.get(&target_module).ok_or_else(|| {
// Import-Map zeigt auf ein nicht im Workspace geladenes // Import map points at a module not loaded in the
// Modul. Das wäre eigentlich schon vom Workspace-Loader // workspace. The workspace loader should have caught
// abgefangen worden; hier defensiv als unknown-module. // this already; defensive unknown-module here.
CheckError::UnknownModule { CheckError::UnknownModule {
module: target_module.clone(), module: target_module.clone(),
} }
@@ -700,9 +701,9 @@ fn synth(
let mut result_ty: Option<Type> = None; let mut result_ty: Option<Type> = None;
for arm in arms { for arm in arms {
// Lokale Bindings sammeln und ins env pushen, body checken, // Collect local bindings and push into the env, check the
// wieder poppen — manuell, weil Patterns mehrere Bindings // body, pop again — done manually because patterns can
// erzeugen können. // produce multiple bindings.
let bindings = type_check_pattern(&arm.pat, &s_ty, env)?; let bindings = type_check_pattern(&arm.pat, &s_ty, env)?;
let mut pushed = Vec::new(); let mut pushed = Vec::new();
for (n, t) in &bindings { for (n, t) in &bindings {
@@ -710,7 +711,7 @@ fn synth(
pushed.push((n.clone(), prev)); pushed.push((n.clone(), prev));
} }
let body_ty = synth(&arm.body, env, locals, effects, in_def)?; let body_ty = synth(&arm.body, env, locals, effects, in_def)?;
// Bindings rückgängig. // Undo bindings.
for (n, prev) in pushed.into_iter().rev() { for (n, prev) in pushed.into_iter().rev() {
match prev { match prev {
Some(p) => { Some(p) => {
@@ -736,7 +737,7 @@ fn synth(
covered_ctors.insert(ctor.clone()); covered_ctors.insert(ctor.clone());
} }
Pattern::Lit { .. } => { Pattern::Lit { .. } => {
// Lit-Patterns decken nichts strukturell ab. // Lit patterns don't structurally cover anything.
} }
} }
} }
@@ -772,8 +773,8 @@ fn synth(
} }
} }
/// Prüft ein Pattern gegen einen Erwartungstyp und gibt die durch das /// Checks a pattern against an expected type and returns the bindings
/// Pattern eingeführten Bindings zurück. /// introduced by the pattern.
fn type_check_pattern( fn type_check_pattern(
p: &Pattern, p: &Pattern,
expected: &Type, expected: &Type,
@@ -793,9 +794,9 @@ fn type_check_pattern(
Ok(vec![]) Ok(vec![])
} }
Pattern::Ctor { ctor, fields } => { Pattern::Ctor { ctor, fields } => {
// MVP: Sub-Patterns von Ctor-Patterns dürfen nur `Var` oder `Wild` // MVP: sub-patterns of ctor patterns may only be `Var` or `Wild`.
// sein. Nested Ctor- oder Lit-Patterns brauchen ein // Nested ctor or lit patterns need decision-tree lowering,
// Decision-Tree-Lowering, das wir im Codegen noch nicht haben. // which we don't have in codegen yet.
for sub in fields { for sub in fields {
if !matches!(sub, Pattern::Var { .. } | Pattern::Wild) { if !matches!(sub, Pattern::Var { .. } | Pattern::Wild) {
return Err(CheckError::NestedCtorPatternNotAllowed(ctor.clone())); return Err(CheckError::NestedCtorPatternNotAllowed(ctor.clone()));
@@ -805,7 +806,7 @@ fn type_check_pattern(
.ctor_index .ctor_index
.get(ctor) .get(ctor)
.ok_or_else(|| CheckError::UnknownCtorInPattern(ctor.clone()))?; .ok_or_else(|| CheckError::UnknownCtorInPattern(ctor.clone()))?;
// expected muss diese ADT sein. // expected must be this ADT.
match expected { match expected {
Type::Con { name } if name == &cref.type_name => {} Type::Con { name } if name == &cref.type_name => {}
_ => { _ => {
@@ -861,18 +862,18 @@ pub struct Env {
pub globals: IndexMap<String, Type>, pub globals: IndexMap<String, Type>,
pub effect_ops: IndexMap<String, builtins::EffectOpSig>, pub effect_ops: IndexMap<String, builtins::EffectOpSig>,
pub types: IndexMap<String, TypeDef>, pub types: IndexMap<String, TypeDef>,
/// Inverser Index: ctor-name -> Verweis auf die zugehörige ADT. /// Inverse index: ctor name -> reference to the owning ADT.
pub ctor_index: IndexMap<String, CtorRef>, pub ctor_index: IndexMap<String, CtorRef>,
/// Import-Map: alias-oder-modulname → tatsächlicher Modulname. /// Import map: alias-or-module-name → actual module name.
/// Genutzt, wenn `Term::Var { name }` einen Punkt enthält /// Used when `Term::Var { name }` contains a dot
/// (qualifizierter Cross-Module-Verweis). /// (qualified cross-module reference).
pub imports: BTreeMap<String, String>, pub imports: BTreeMap<String, String>,
/// Pro Modul des Workspaces dessen Top-Level-Symboltabelle. /// Top-level symbol table per module of the workspace.
/// `check_in_workspace` befüllt das aus `build_module_globals`. /// `check_in_workspace` populates this from `build_module_globals`.
pub module_globals: BTreeMap<String, IndexMap<String, Type>>, pub module_globals: BTreeMap<String, IndexMap<String, Type>>,
/// Name des aktuell gecheckten Moduls. Genutzt, um beim Var-Lookup /// Name of the currently checked module. Used during var lookup to
/// Selbst-Verweise (Modulname == eigener Name) als lokale Globals zu /// treat self-references (module name == own name) as local globals,
/// behandeln, ohne den `imports`-Kanal anzufassen. /// without touching the `imports` channel.
pub current_module: String, pub current_module: String,
} }
@@ -963,7 +964,7 @@ mod tests {
Type::Fn { Type::Fn {
params: vec![], params: vec![],
ret: Box::new(Type::unit()), ret: Box::new(Type::unit()),
effects: vec![], // !IO fehlt effects: vec![], // !IO missing
}, },
vec![], vec![],
Term::Do { Term::Do {
@@ -1007,7 +1008,7 @@ mod tests {
#[test] #[test]
fn match_must_be_exhaustive() { fn match_must_be_exhaustive() {
// Type Maybe = None | Some(Int); fn f matches nur None -> Fehler. // Type Maybe = None | Some(Int); fn f only matches None -> error.
let m = Module { let m = Module {
schema: SCHEMA.into(), schema: SCHEMA.into(),
name: "t".into(), name: "t".into(),
+13 -13
View File
@@ -1,8 +1,8 @@
//! Integrationstests für `check_workspace` (Iter 5b). //! Integration tests for `check_workspace` (Iter 5b).
//! //!
//! Diese Tests fahren über die kanonischen `examples/ws_*.ail.json`-Files; //! These tests drive the canonical `examples/ws_*.ail.json` files;
//! der Loader und der Checker zusammen sind die Pipeline, die `ail check` //! the loader and the checker together form the pipeline that `ail check`
//! im JSON-Modus auf einen Workspace wirft. //! runs in JSON mode against a workspace.
use ailang_check::{check_workspace, Severity}; use ailang_check::{check_workspace, Severity};
use ailang_core::load_workspace; use ailang_core::load_workspace;
@@ -15,8 +15,8 @@ fn examples_dir() -> std::path::PathBuf {
#[test] #[test]
fn happy_path_resolves_qualified_import() { fn happy_path_resolves_qualified_import() {
// ws_main importiert ws_lib und ruft `ws_lib.add` auf — vollständig // ws_main imports ws_lib and calls `ws_lib.add` — fully typed.
// typisiert. Erwartet: keine Diagnostics. // Expected: no diagnostics.
let entry = examples_dir().join("ws_main.ail.json"); let entry = examples_dir().join("ws_main.ail.json");
let ws = load_workspace(&entry).expect("load ws_main"); let ws = load_workspace(&entry).expect("load ws_main");
let diags = check_workspace(&ws); let diags = check_workspace(&ws);
@@ -29,14 +29,14 @@ fn happy_path_resolves_qualified_import() {
#[test] #[test]
fn unknown_import_is_reported() { fn unknown_import_is_reported() {
// ws_broken referenziert `ws_lib.bogus` — Modul ist da, Def nicht. // ws_broken references `ws_lib.bogus` — module is there, def isn't.
let entry = examples_dir().join("ws_broken.ail.json"); let entry = examples_dir().join("ws_broken.ail.json");
let ws = load_workspace(&entry).expect("load ws_broken"); let ws = load_workspace(&entry).expect("load ws_broken");
let diags = check_workspace(&ws); let diags = check_workspace(&ws);
assert_eq!(diags.len(), 1, "got: {:?}", diags); assert_eq!(diags.len(), 1, "got: {:?}", diags);
assert!(matches!(diags[0].severity, Severity::Error)); assert!(matches!(diags[0].severity, Severity::Error));
assert_eq!(diags[0].code, "unknown-import"); assert_eq!(diags[0].code, "unknown-import");
// Kontext muss strukturiert auf Modul + Defname zeigen. // Context must point structurally to module + def name.
assert_eq!( assert_eq!(
diags[0].ctx.get("module").and_then(|v| v.as_str()), diags[0].ctx.get("module").and_then(|v| v.as_str()),
Some("ws_lib") Some("ws_lib")
@@ -49,7 +49,7 @@ fn unknown_import_is_reported() {
#[test] #[test]
fn unknown_module_prefix_is_reported() { fn unknown_module_prefix_is_reported() {
// ws_unknown_module hat keine Imports, referenziert aber `nope.x`. // ws_unknown_module has no imports but references `nope.x`.
let entry = examples_dir().join("ws_unknown_module.ail.json"); let entry = examples_dir().join("ws_unknown_module.ail.json");
let ws = load_workspace(&entry).expect("load ws_unknown_module"); let ws = load_workspace(&entry).expect("load ws_unknown_module");
let diags = check_workspace(&ws); let diags = check_workspace(&ws);
@@ -64,10 +64,10 @@ fn unknown_module_prefix_is_reported() {
#[test] #[test]
fn invalid_def_name_with_dot_is_reported() { fn invalid_def_name_with_dot_is_reported() {
// Synthetic: ein Modul mit einer Def, deren Name einen Punkt enthält. // Synthetic: a module with a def whose name contains a dot.
// Wir konstruieren das als Module direkt und füttern es in einen // We construct this as a Module directly and feed it into a
// Trivial-Workspace, weil die kanonische Konvention das gar nicht // trivial workspace, because the canonical convention should not
// erst auf Disk durchlassen sollte. // let this through to disk in the first place.
use ailang_core::ast::*; use ailang_core::ast::*;
use std::collections::BTreeMap; use std::collections::BTreeMap;
+98 -98
View File
@@ -1,24 +1,24 @@
//! LLVM-IR-Text-Emitter für AILang (MVP). //! LLVM IR text emitter for AILang (MVP).
//! //!
//! Strategie: Wir erzeugen LLVM-IR als String, schreiben sie als `.ll` und //! Strategy: we generate LLVM IR as a string, write it as `.ll`, and
//! linken sie mit `clang`. Keine Bindung an eine bestimmte libllvm-Version. //! link it with `clang`. No binding to a specific libllvm version.
//! //!
//! Typ-Mapping: //! Type mapping:
//! - `Int` -> `i64` //! - `Int` -> `i64`
//! - `Bool` -> `i1` //! - `Bool` -> `i1`
//! - `Unit` -> `i8` (Wert immer 0) //! - `Unit` -> `i8` (value always 0)
//! //!
//! Mangling-Schema (Iter 5c): //! Mangling scheme (Iter 5c):
//! - **Alle** AILang-Funktionen werden zu `@ail_<modul>_<def>`. Das gilt //! - **All** AILang functions become `@ail_<module>_<def>`. This holds
//! auch für Single-Modul-Programme. Die alte Form `@ail_<def>` entfällt. //! even for single-module programs. The old form `@ail_<def>` is gone.
//! - Globale String-/Konstanten-Symbole sind pro Modul gemangelt: //! - Global string/constant symbols are mangled per module:
//! `@.str_<modul>_<idx>` bzw. `@ail_<modul>_<def>` für Konstanten-Globals. //! `@.str_<module>_<idx>` and `@ail_<module>_<def>` for constant globals.
//! - Eintrittspunkt bleibt `main` (LLVM-/C-ABI). Der Generator emittiert //! - The entry point remains `main` (LLVM/C ABI). The generator emits
//! `define i64 @main() { call @ail_<entry-modul>_main() ... }` als //! `define i64 @main() { call @ail_<entry-module>_main() ... }` as a
//! Trampoline auf das `main` des Eintrittsmoduls. Fehlt dieses, scheitert //! trampoline to the entry module's `main`. If missing, the build
//! der Build mit `MissingEntryMain`. //! fails with `MissingEntryMain`.
//! - `source_filename` taucht genau einmal am Anfang auf, mit //! - `source_filename` appears exactly once at the top, with
//! `<entry-modul>.ail` als Wert (pro Workspace). //! `<entry-module>.ail` as value (per workspace).
use ailang_core::ast::*; use ailang_core::ast::*;
use ailang_core::Workspace; use ailang_core::Workspace;
@@ -50,10 +50,10 @@ pub enum CodegenError {
type Result<T> = std::result::Result<T, CodegenError>; type Result<T> = std::result::Result<T, CodegenError>;
/// Lowert einen einzelnen Modul. Backwards-Kompatibilität für Tests / CLI- /// Lowers a single module. Backwards compatibility for tests / CLI calls
/// Aufrufe, die ohne Workspace arbeiten möchten. Intern wird ein Trivial- /// that want to operate without a workspace. Internally a trivial
/// Workspace mit nur diesem Modul gebaut, sodass das Mangling-Schema /// workspace with just this module is built, so the mangling scheme
/// konsistent bleibt. /// stays consistent.
pub fn emit_ir(m: &Module) -> Result<String> { pub fn emit_ir(m: &Module) -> Result<String> {
let mut modules = BTreeMap::new(); let mut modules = BTreeMap::new();
modules.insert(m.name.clone(), m.clone()); modules.insert(m.name.clone(), m.clone());
@@ -65,21 +65,21 @@ pub fn emit_ir(m: &Module) -> Result<String> {
lower_workspace(&ws) lower_workspace(&ws)
} }
/// Lowert einen kompletten Workspace zu einem `.ll`-String. Reihenfolge der /// Lowers a whole workspace to a `.ll` string. Module order is
/// Module ist alphabetisch (BTreeMap-Order = deterministisch). Innerhalb /// alphabetic (BTreeMap order = deterministic). Within a module, def
/// eines Moduls bleibt die Def-Reihenfolge wie im AST. /// order matches the AST.
/// ///
/// Cross-Module-Calls: `Term::Var { name }` mit genau einem Punkt /// Cross-module calls: `Term::Var { name }` with exactly one dot
/// (`<prefix>.<def>`) wird über die Import-Map des aufrufenden Moduls auf /// (`<prefix>.<def>`) is resolved via the calling module's import map
/// `@ail_<echtes_modul>_<def>` aufgelöst. Lokale Var-Lookups (ohne Punkt) /// to `@ail_<actual_module>_<def>`. Local var lookups (no dot) stay
/// bleiben Stack-Locals oder lokale Top-Level-Defs des aktuellen Moduls. /// stack locals or local top-level defs of the current module.
pub fn lower_workspace(ws: &Workspace) -> Result<String> { pub fn lower_workspace(ws: &Workspace) -> Result<String> {
let mut header = String::new(); let mut header = String::new();
let mut body = String::new(); let mut body = String::new();
let mut all_strings: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new(); let mut all_strings: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
// ^ pro Modul: Liste (global-name, content). Reihenfolge = Insert-Order. // ^ per module: list of (global-name, content). Order = insertion order.
// Pass 1: Pro Modul die Top-Level-Symboltabelle (für lokale Var-Lookups). // Pass 1: per-module top-level symbol table (for local var lookups).
let mut module_user_fns: BTreeMap<String, BTreeMap<String, FnSig>> = BTreeMap::new(); let mut module_user_fns: BTreeMap<String, BTreeMap<String, FnSig>> = BTreeMap::new();
for (mname, m) in &ws.modules { for (mname, m) in &ws.modules {
let mut user_fns = BTreeMap::new(); let mut user_fns = BTreeMap::new();
@@ -97,12 +97,12 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
module_user_fns.insert(mname.clone(), user_fns); module_user_fns.insert(mname.clone(), user_fns);
} }
// Pass 2: pro Modul lowern. Globals/Strings werden pro Modul akkumuliert, // Pass 2: lower per module. Globals/strings are accumulated per module,
// weil sie pro-Modul gemangelt sind. // because they are mangled per module.
for (mname, m) in &ws.modules { for (mname, m) in &ws.modules {
// Import-Map für Cross-Module-Resolution. Identisch zur // Import map for cross-module resolution. Identical to the
// Logik im Typchecker (siehe `check_in_workspace`): Alias oder // logic in the typechecker (see `check_in_workspace`): alias or
// Modulname als Key, echter Modulname als Value. // module name as key, actual module name as value.
let mut import_map: BTreeMap<String, String> = BTreeMap::new(); let mut import_map: BTreeMap<String, String> = BTreeMap::new();
for imp in &m.imports { for imp in &m.imports {
let key = imp.alias.clone().unwrap_or_else(|| imp.module.clone()); let key = imp.alias.clone().unwrap_or_else(|| imp.module.clone());
@@ -115,19 +115,19 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
.map_err(|e| CodegenError::InModule(mname.clone(), Box::new(e)))?; .map_err(|e| CodegenError::InModule(mname.clone(), Box::new(e)))?;
header.push_str(&emitter.header); header.push_str(&emitter.header);
body.push_str(&emitter.body); body.push_str(&emitter.body);
// Strings sammeln, in Insertion-Order. // Collect strings in insertion order.
let mut entries: Vec<(String, String)> = Vec::new(); let mut entries: Vec<(String, String)> = Vec::new();
for (content, (name, _)) in &emitter.strings { for (content, (name, _)) in &emitter.strings {
entries.push((name.clone(), content.clone())); entries.push((name.clone(), content.clone()));
} }
// sort by global name to stay deterministic across runs (intern_string // sort by global name to stay deterministic across runs (intern_string
// benutzt einen monotonen Counter, also reicht alphabetisch). // uses a monotonic counter, so alphabetic is enough).
entries.sort_by(|a, b| a.0.cmp(&b.0)); entries.sort_by(|a, b| a.0.cmp(&b.0));
all_strings.insert(mname.clone(), entries); all_strings.insert(mname.clone(), entries);
} }
// Trampoline: prüfe, dass das Eintrittsmodul ein `main : () -> Unit !IO` // Trampoline: verify that the entry module has a
// hat. Wenn nicht, ist der Workspace nicht ausführbar. // `main : () -> Unit !IO`. If not, the workspace isn't runnable.
let entry_module = ws let entry_module = ws
.modules .modules
.get(&ws.entry) .get(&ws.entry)
@@ -151,8 +151,8 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
out.push_str(default_triple()); out.push_str(default_triple());
out.push_str("\"\n\n"); out.push_str("\"\n\n");
// Globals: pro Modul, alphabetisch über Modulnamen (BTreeMap-Order), // Globals: per module, alphabetically over module names (BTreeMap order),
// dann Insertion-Order pro Modul. // then insertion order per module.
let mut emitted_global = false; let mut emitted_global = false;
for (_mname, entries) in &all_strings { for (_mname, entries) in &all_strings {
for (name, content) in entries { for (name, content) in entries {
@@ -195,32 +195,32 @@ fn main_is_void(t: &Type) -> bool {
struct Emitter<'a> { struct Emitter<'a> {
module: &'a Module, module: &'a Module,
/// Name des aktuell gelowerten Moduls (für Mangling). /// Name of the currently lowered module (for mangling).
module_name: &'a str, module_name: &'a str,
header: String, header: String,
body: String, body: String,
/// String-Konstanten: content -> (global-name (ohne `@`), llvm-typ-länge inkl. \0) /// String constants: content -> (global name (without `@`), llvm type length incl. \0)
strings: BTreeMap<String, (String, usize)>, strings: BTreeMap<String, (String, usize)>,
/// Lokale Symboltabelle pro Funktion: name -> (ssa-name inkl `%`, llvm-typ). /// Local symbol table per function: name -> (ssa name incl. `%`, llvm type).
locals: Vec<(String, String, String)>, locals: Vec<(String, String, String)>,
/// fortlaufender Zähler für SSA-Werte und Labels. /// Monotonic counter for SSA values and labels.
counter: u64, counter: u64,
/// fortlaufender Zähler für globale String-Namen (pro Modul). /// Monotonic counter for global string names (per module).
str_counter: u64, str_counter: u64,
/// Top-Level-Funktionen pro Modul des Workspaces, für call-resolution. /// Top-level functions per module of the workspace, for call resolution.
module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>, module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>,
/// Import-Map des aktuellen Moduls (Alias/Modulname → echter Modulname). /// Import map of the current module (alias/module name → actual module name).
import_map: BTreeMap<String, String>, import_map: BTreeMap<String, String>,
/// ADT-Tabelle: type_name -> Liste von ctors in Definition-Reihenfolge. /// ADT table: type_name -> list of ctors in definition order.
/// Tag eines ctors = Index in dieser Liste. Wird in `ctor_index` /// Tag of a ctor = index in this list. Replicated in `ctor_index`;
/// repliziert; behalten für künftige Tools (Pretty-Printer für ADT-Werte, /// kept around for future tools (pretty-printer for ADT values,
/// Decision-Tree-Optimierung). /// decision-tree optimization).
#[allow(dead_code)] #[allow(dead_code)]
types: BTreeMap<String, Vec<CtorInfo>>, types: BTreeMap<String, Vec<CtorInfo>>,
/// Inverser Index: ctor-name -> (type_name, tag, field_llvm_types). /// Inverse index: ctor name -> (type_name, tag, field_llvm_types).
ctor_index: BTreeMap<String, CtorRef>, ctor_index: BTreeMap<String, CtorRef>,
/// Aktuelles Basic-Block-Label. Wird von `start_block` gesetzt und ist /// Current basic block label. Set by `start_block` and is
/// die einzige Quelle der Wahrheit für `phi`-Operanden. /// the single source of truth for `phi` operands.
current_block: String, current_block: String,
} }
@@ -315,9 +315,9 @@ impl<'a> Emitter<'a> {
.map_err(|e| CodegenError::Def(c.name.clone(), Box::new(e)))?; .map_err(|e| CodegenError::Def(c.name.clone(), Box::new(e)))?;
} }
Def::Type(_) => { Def::Type(_) => {
// Keine LLVM-Definition nötig: die ADT existiert nur als // No LLVM definition needed: the ADT exists only as a
// logischer Typ. Heap-Boxen werden ad-hoc per malloc // logical type. Heap boxes are allocated ad hoc via
// angelegt. // malloc.
} }
} }
} }
@@ -330,7 +330,7 @@ impl<'a> Emitter<'a> {
Term::Lit { lit } => lit, Term::Lit { lit } => lit,
_ => { _ => {
return Err(CodegenError::Internal( return Err(CodegenError::Internal(
"MVP: const muss Literal sein".into(), "MVP: const must be a literal".into(),
)); ));
} }
}; };
@@ -389,7 +389,7 @@ impl<'a> Emitter<'a> {
if i > 0 { if i > 0 {
sig.push_str(", "); sig.push_str(", ");
} }
// SSA-Argumentname: %arg_<name> // SSA argument name: %arg_<name>
sig.push_str(&format!("{} %arg_{}", pty, pname)); sig.push_str(&format!("{} %arg_{}", pty, pname));
self.locals.push(( self.locals.push((
pname.clone(), pname.clone(),
@@ -413,7 +413,7 @@ impl<'a> Emitter<'a> {
Ok(()) Ok(())
} }
/// Lowert einen Term zu (SSA-Value-String, LLVM-Typ). /// Lowers a term to (SSA value string, LLVM type).
fn lower_term(&mut self, t: &Term) -> Result<(String, String)> { fn lower_term(&mut self, t: &Term) -> Result<(String, String)> {
match t { match t {
Term::Lit { lit } => Ok(match lit { Term::Lit { lit } => Ok(match lit {
@@ -423,8 +423,8 @@ impl<'a> Emitter<'a> {
"i1".into(), "i1".into(),
), ),
Literal::Str { value } => { Literal::Str { value } => {
// Globale Konstante anlegen; in opaque-pointer-LLVM // Create global constant; in opaque-pointer LLVM,
// ist `@name` direkt ein gültiger `ptr`. // `@name` is directly a valid `ptr`.
let g = self.intern_string("str", value); let g = self.intern_string("str", value);
(format!("@{g}"), "ptr".into()) (format!("@{g}"), "ptr".into())
} }
@@ -462,8 +462,8 @@ impl<'a> Emitter<'a> {
self.start_block(&then_lbl); self.start_block(&then_lbl);
let (then_v, then_ty) = self.lower_term(then)?; let (then_v, then_ty) = self.lower_term(then)?;
// Verschachtelter Code im `then`-Body kann das Block-Label // Nested code in the `then` body may have changed the
// verändert haben — phi muss den letzten tatsächlichen Block sehen. // block label — phi must see the last actual block.
let then_block_end = self.current_block.clone(); let then_block_end = self.current_block.clone();
self.body.push_str(&format!(" br label %{join_lbl}\n")); self.body.push_str(&format!(" br label %{join_lbl}\n"));
@@ -494,7 +494,7 @@ impl<'a> Emitter<'a> {
Term::Var { name } => name.clone(), Term::Var { name } => name.clone(),
_ => { _ => {
return Err(CodegenError::Internal( return Err(CodegenError::Internal(
"MVP: callee muss Variable sein".into(), "MVP: callee must be a variable".into(),
)); ));
} }
}; };
@@ -506,9 +506,9 @@ impl<'a> Emitter<'a> {
} }
} }
/// Heap-Box-Layout: 8 Bytes Tag (i64) gefolgt von je 8 Bytes pro Feld. /// Heap box layout: 8 bytes tag (i64) followed by 8 bytes per field.
/// Auch i1- und i8-Felder belegen einen vollen 8-Byte-Slot — die typed /// i1 and i8 fields also occupy a full 8-byte slot — the typed
/// load/store-Instruktionen schreiben/lesen nur die erforderliche Größe. /// load/store instructions write/read only the required size.
fn lower_ctor( fn lower_ctor(
&mut self, &mut self,
type_name: &str, type_name: &str,
@@ -535,8 +535,8 @@ impl<'a> Emitter<'a> {
"ctor `{type_name}/{ctor_name}` arity" "ctor `{type_name}/{ctor_name}` arity"
))); )));
} }
// Argumente vorab auswerten, damit Allocation und Store nahe beieinander // Evaluate arguments up front so allocation and store stay close
// bleiben. // together.
let mut compiled = Vec::new(); let mut compiled = Vec::new();
for (a, exp) in args.iter().zip(cref.fields.iter()) { for (a, exp) in args.iter().zip(cref.fields.iter()) {
let (v, vty) = self.lower_term(a)?; let (v, vty) = self.lower_term(a)?;
@@ -552,12 +552,12 @@ impl<'a> Emitter<'a> {
self.body.push_str(&format!( self.body.push_str(&format!(
" {p} = call ptr @malloc(i64 {size_bytes})\n" " {p} = call ptr @malloc(i64 {size_bytes})\n"
)); ));
// Tag schreiben. // Write tag.
self.body.push_str(&format!( self.body.push_str(&format!(
" store i64 {tag}, ptr {p}, align 8\n", " store i64 {tag}, ptr {p}, align 8\n",
tag = cref.tag tag = cref.tag
)); ));
// Felder schreiben. // Write fields.
for (i, (v, ty)) in compiled.iter().enumerate() { for (i, (v, ty)) in compiled.iter().enumerate() {
let off = 8 + i as i64 * 8; let off = 8 + i as i64 * 8;
let addr = self.fresh_ssa(); let addr = self.fresh_ssa();
@@ -578,16 +578,16 @@ impl<'a> Emitter<'a> {
let (s_val, s_ty) = self.lower_term(scrutinee)?; let (s_val, s_ty) = self.lower_term(scrutinee)?;
if s_ty != "ptr" { if s_ty != "ptr" {
return Err(CodegenError::Internal(format!( return Err(CodegenError::Internal(format!(
"match auf nicht-ADT scrutinee (got {s_ty}); MVP unterstützt nur ADTs" "match on non-ADT scrutinee (got {s_ty}); MVP supports only ADTs"
))); )));
} }
// Tag laden. // Load tag.
let tag = self.fresh_ssa(); let tag = self.fresh_ssa();
self.body self.body
.push_str(&format!(" {tag} = load i64, ptr {s_val}, align 8\n")); .push_str(&format!(" {tag} = load i64, ptr {s_val}, align 8\n"));
// Arms separieren. // Separate arms.
let mut ctor_arms: Vec<(CtorRef, &Arm, Vec<Option<String>>)> = Vec::new(); let mut ctor_arms: Vec<(CtorRef, &Arm, Vec<Option<String>>)> = Vec::new();
let mut open_arm: Option<&Arm> = None; let mut open_arm: Option<&Arm> = None;
let mut open_var: Option<String> = None; let mut open_var: Option<String> = None;
@@ -615,14 +615,14 @@ impl<'a> Emitter<'a> {
.map(|p| match p { .map(|p| match p {
Pattern::Var { name } => Some(name.clone()), Pattern::Var { name } => Some(name.clone()),
Pattern::Wild => None, Pattern::Wild => None,
_ => None, // MVP: nested ctor/lit patterns nicht supported _ => None, // MVP: nested ctor/lit patterns not supported
}) })
.collect(); .collect();
ctor_arms.push((cref, arm, bindings)); ctor_arms.push((cref, arm, bindings));
} }
Pattern::Lit { .. } => { Pattern::Lit { .. } => {
return Err(CodegenError::Internal( return Err(CodegenError::Internal(
"MVP: Lit-Patterns in Match nicht unterstützt".into(), "MVP: lit patterns in match not supported".into(),
)); ));
} }
} }
@@ -651,7 +651,7 @@ impl<'a> Emitter<'a> {
for (i, (cref, arm, bindings)) in ctor_arms.iter().enumerate() { for (i, (cref, arm, bindings)) in ctor_arms.iter().enumerate() {
self.start_block(&arm_labels[i]); self.start_block(&arm_labels[i]);
// Felder laden und als locals binden. // Load fields and bind as locals.
let mut pushed = 0usize; let mut pushed = 0usize;
for (idx, (binding, fty)) in for (idx, (binding, fty)) in
bindings.iter().zip(cref.fields.iter()).enumerate() bindings.iter().zip(cref.fields.iter()).enumerate()
@@ -672,7 +672,7 @@ impl<'a> Emitter<'a> {
} }
} }
let (val, vty) = self.lower_term(&arm.body)?; let (val, vty) = self.lower_term(&arm.body)?;
// bindings poppen // pop bindings
for _ in 0..pushed { for _ in 0..pushed {
self.locals.pop(); self.locals.pop();
} }
@@ -690,10 +690,10 @@ impl<'a> Emitter<'a> {
} }
} }
// default-block // default block
self.start_block(&default_lbl); self.start_block(&default_lbl);
if let Some(arm) = open_arm { if let Some(arm) = open_arm {
// ggf. var-binding einrichten // set up var binding if needed
let pushed = if let Some(name) = open_var.take() { let pushed = if let Some(name) = open_var.take() {
self.locals.push((name, s_val.clone(), "ptr".into())); self.locals.push((name, s_val.clone(), "ptr".into()));
1 1
@@ -717,7 +717,7 @@ impl<'a> Emitter<'a> {
result_ty = Some(vty); result_ty = Some(vty);
} }
} else { } else {
// Typchecker garantiert Exhaustiveness, also unreachable. // Typechecker guarantees exhaustiveness, so unreachable.
self.body.push_str(" unreachable\n"); self.body.push_str(" unreachable\n");
} }
@@ -763,8 +763,8 @@ impl<'a> Emitter<'a> {
return Ok((dst, "i1".into())); return Ok((dst, "i1".into()));
} }
// Cross-Module-Call: genau ein Punkt im Namenüber Import-Map auflösen. // Cross-module call: exactly one dot in the name → resolve via import map.
// Logik identisch zum Typchecker (siehe `synth` für `Term::Var`). // Logic identical to the typechecker (see `synth` for `Term::Var`).
if name.matches('.').count() == 1 { if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked"); let (prefix, suffix) = name.split_once('.').expect("checked");
let target_module = self.import_map.get(prefix).cloned().ok_or_else(|| { let target_module = self.import_map.get(prefix).cloned().ok_or_else(|| {
@@ -791,7 +791,7 @@ impl<'a> Emitter<'a> {
return self.emit_call(&target_module, suffix, &sig, args); return self.emit_call(&target_module, suffix, &sig, args);
} }
// User-Funktion im aktuellen Modul? // User function in the current module?
if let Some(sig) = self if let Some(sig) = self
.module_user_fns .module_user_fns
.get(self.module_name) .get(self.module_name)
@@ -886,7 +886,7 @@ impl<'a> Emitter<'a> {
"io/print_bool needs i1".into(), "io/print_bool needs i1".into(),
)); ));
} }
// Drucke "true\n" oder "false\n". // Print "true\n" or "false\n".
let fmt_t = self.intern_string("fmt_true", "true\n"); let fmt_t = self.intern_string("fmt_true", "true\n");
let fmt_f = self.intern_string("fmt_false", "false\n"); let fmt_f = self.intern_string("fmt_false", "false\n");
let id = self.fresh_id(); let id = self.fresh_id();
@@ -928,7 +928,7 @@ impl<'a> Emitter<'a> {
if let Some((name, _)) = self.strings.get(content) { if let Some((name, _)) = self.strings.get(content) {
return name.clone(); return name.clone();
} }
// Mangling pro Modul: `.str_<modul>_<hint>_<idx>`. // Mangling per module: `.str_<module>_<hint>_<idx>`.
let name = format!(".str_{}_{}_{}", self.module_name, hint, self.str_counter); let name = format!(".str_{}_{}_{}", self.module_name, hint, self.str_counter);
self.str_counter += 1; self.str_counter += 1;
let len = c_byte_len(content); let len = c_byte_len(content);
@@ -945,9 +945,9 @@ fn llvm_type(t: &Type) -> Result<String> {
"Bool" => Ok("i1".into()), "Bool" => Ok("i1".into()),
"Unit" => Ok("i8".into()), "Unit" => Ok("i8".into()),
"Str" => Ok("ptr".into()), "Str" => Ok("ptr".into()),
// Alle anderen Type-Namen werden als ADT (Boxed) behandelt. // All other type names are treated as ADT (boxed).
// Falls der Typchecker nicht vorher abgelehnt hat, ist das // If the typechecker didn't reject this earlier, it's
// beabsichtigt — sonst würde `ptr` einen falschen Wert maskieren. // intentional — otherwise `ptr` would mask a wrong value.
_ => Ok("ptr".into()), _ => Ok("ptr".into()),
}, },
other => Err(CodegenError::UnsupportedType( other => Err(CodegenError::UnsupportedType(
@@ -977,11 +977,11 @@ fn c_byte_len(s: &str) -> usize {
s.as_bytes().len() + 1 // + NUL s.as_bytes().len() + 1 // + NUL
} }
/// Escapt einen String für LLVM IR `c"..."`. Alle Bytes außerhalb von /// Escapes a string for LLVM IR `c"..."`. All bytes outside
/// 0x20..0x7E werden als `\HH` escapt; `"` und `\` ebenfalls. Endet mit `\00`. /// 0x20..0x7E are escaped as `\HH`; `"` and `\` likewise. Ends with `\00`.
fn default_triple() -> &'static str { fn default_triple() -> &'static str {
// Im MVP fragen wir den Compile-Host. Für Cross-Compilation müsste man das // In the MVP we query the compile host. For cross-compilation this
// konfigurierbar machen — kein Bedarf jetzt. // would need to be configurable — not needed now.
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") { if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
"x86_64-pc-linux-gnu" "x86_64-pc-linux-gnu"
} else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
@@ -1016,8 +1016,8 @@ mod tests {
#[test] #[test]
fn emits_arith_fn() { fn emits_arith_fn() {
// Single-Modul wird via `emit_ir` zu Trivial-Workspace; das Mangling // Single module becomes a trivial workspace via `emit_ir`; the
// lautet `@ail_<modul>_<def>` auch im Single-File-Fall. // mangling is `@ail_<module>_<def>` even in the single-file case.
let m = Module { let m = Module {
schema: SCHEMA.into(), schema: SCHEMA.into(),
name: "t".into(), name: "t".into(),
@@ -1040,8 +1040,8 @@ mod tests {
}, },
doc: None, doc: None,
}), }),
// Eintrittsmodul braucht ein `main`, sonst liefert // Entry module needs a `main`, otherwise
// `lower_workspace` `MissingEntryMain`. // `lower_workspace` returns `MissingEntryMain`.
Def::Fn(FnDef { Def::Fn(FnDef {
name: "main".into(), name: "main".into(),
ty: Type::Fn { ty: Type::Fn {
+12 -12
View File
@@ -1,4 +1,4 @@
//! AST-Knoten. Serde-Layout entspricht dem JSON-Schema in `docs/DESIGN.md`. //! AST nodes. Serde layout matches the JSON schema in `docs/DESIGN.md`.
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -36,14 +36,14 @@ impl Def {
} }
} }
/// Externer Helper: Name einer Definition (für Tools wie `ail diff`, /// External helper: name of a definition (for tools like `ail diff`
/// die den Def-Knoten entkoppelt vom Methoden-Aufruf konsumieren). /// that consume the def node decoupled from the method call).
pub fn def_name(def: &Def) -> &str { pub fn def_name(def: &Def) -> &str {
def.name() def.name()
} }
/// Externer Helper: Diskriminator-Tag einer Definition (`fn`, `const`, `type`). /// External helper: discriminator tag of a definition (`fn`, `const`, `type`).
/// Identisch mit dem `kind`-Feld in der JSON-Repräsentation. /// Identical to the `kind` field in the JSON representation.
pub fn def_kind(def: &Def) -> &'static str { pub fn def_kind(def: &Def) -> &'static str {
match def { match def {
Def::Fn(_) => "fn", Def::Fn(_) => "fn",
@@ -113,8 +113,8 @@ pub enum Term {
op: String, op: String,
args: Vec<Term>, args: Vec<Term>,
}, },
/// Konstruktor-Anwendung. `type_name` bindet die ADT an, `ctor` den Variant. /// Constructor application. `type_name` binds the ADT, `ctor` the variant.
/// Beispiel: `Some(42)` -> `{ "t": "ctor", "type": "Option", "ctor": "Some", /// Example: `Some(42)` -> `{ "t": "ctor", "type": "Option", "ctor": "Some",
/// "args": [{"t":"lit","lit":{"kind":"int","value":42}}] }`. /// "args": [{"t":"lit","lit":{"kind":"int","value":42}}] }`.
Ctor { Ctor {
#[serde(rename = "type")] #[serde(rename = "type")]
@@ -123,7 +123,7 @@ pub enum Term {
#[serde(default)] #[serde(default)]
args: Vec<Term>, args: Vec<Term>,
}, },
/// Pattern matching über einen Wert. /// Pattern matching over a value.
Match { Match {
scrutinee: Box<Term>, scrutinee: Box<Term>,
arms: Vec<Arm>, arms: Vec<Arm>,
@@ -139,13 +139,13 @@ pub struct Arm {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "p", rename_all = "lowercase")] #[serde(tag = "p", rename_all = "lowercase")]
pub enum Pattern { pub enum Pattern {
/// `_` — bindet nichts, matcht alles. /// `_` — binds nothing, matches everything.
Wild, Wild,
/// `x` — bindet den Wert an einen Namen. /// `x` — binds the value to a name.
Var { name: String }, Var { name: String },
/// Match auf ein Literal. /// Match on a literal.
Lit { lit: Literal }, Lit { lit: Literal },
/// Match auf einen Konstruktor mit Sub-Patterns für seine Felder. /// Match on a constructor with sub-patterns for its fields.
Ctor { Ctor {
ctor: String, ctor: String,
#[serde(default)] #[serde(default)]
+4 -4
View File
@@ -1,11 +1,11 @@
//! Kanonische JSON-Serialisierung. //! Canonical JSON serialization.
//! //!
//! Schreibt JSON ohne Whitespace und mit lexikographisch sortierten Object-Keys. //! Writes JSON without whitespace and with lexicographically sorted object keys.
//! Damit ist die Repräsentation deterministisch und für Hashing geeignet. //! This makes the representation deterministic and suitable for hashing.
use std::io::Write; use std::io::Write;
/// Kanonische Bytes für ein beliebiges Serializable-Objekt. /// Canonical bytes for any Serializable object.
pub fn to_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> { pub fn to_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> {
let v = serde_json::to_value(value).expect("serializable"); let v = serde_json::to_value(value).expect("serializable");
let mut out = Vec::new(); let mut out = Vec::new();
+6 -6
View File
@@ -1,14 +1,14 @@
//! Content-addressed Hashing für Definitionen. //! Content-addressed hashing for definitions.
//! //!
//! Hash = BLAKE3 über die kanonische JSON-Form (siehe `canonical`). //! Hash = BLAKE3 over the canonical JSON form (see `canonical`).
//! Das `hash`-Feld in der Eingabe wird vor dem Hashen entfernt. //! The `hash` field in the input is removed before hashing.
use crate::ast::Def; use crate::ast::Def;
use crate::canonical; use crate::canonical;
/// 16-Hex-Zeichen (64 bit) Prefix des BLAKE3-Hashes. /// 16-hex-char (64-bit) prefix of the BLAKE3 hash.
/// Reicht für Eindeutigkeit innerhalb realistischer Codebases und ist /// Enough for uniqueness within realistic codebases and compact
/// kompakt genug für visuelle Inspektion. /// enough for visual inspection.
pub fn def_hash(def: &Def) -> String { pub fn def_hash(def: &Def) -> String {
let bytes = canonical::to_bytes(def); let bytes = canonical::to_bytes(def);
let h = blake3::hash(&bytes); let h = blake3::hash(&bytes);
+3 -3
View File
@@ -1,7 +1,7 @@
//! AILang Kerndatenmodell. //! AILang core data model.
//! //!
//! Quelle einer AILang-Übersetzungseinheit ist ein `Module` als JSON. Dieses //! The source of an AILang translation unit is a `Module` as JSON. This
//! Crate definiert das Schema, Serialisierung und content-addressed Hashing. //! crate defines the schema, serialization, and content-addressed hashing.
pub mod ast; pub mod ast;
pub mod canonical; pub mod canonical;
+8 -8
View File
@@ -1,8 +1,8 @@
//! Pretty-Printer: AST → menschenlesbare Textform. //! Pretty-printer: AST → human-readable text form.
//! //!
//! Die Textform ist als Diff- und Review-Werkzeug gedacht. Die //! The text form is intended as a diff and review tool. The
//! kanonische Quelle bleibt die JSON-Form. Jede pretty-Ausgabe ist //! canonical source remains the JSON form. Every pretty output is
//! deterministisch. //! deterministic.
use crate::ast::*; use crate::ast::*;
use std::fmt::Write; use std::fmt::Write;
@@ -254,10 +254,10 @@ fn term_inline(t: &Term) -> String {
s s
} }
Term::Match { scrutinee, .. } => { Term::Match { scrutinee, .. } => {
// Match nicht inline darstellen — mit Marker. // Don't render match inline — use a marker.
format!("(match {} ...)", term_inline(scrutinee)) format!("(match {} ...)", term_inline(scrutinee))
} }
// Strukturelle Terms in Inline-Form rekursiv schwer; fallback: // Structural terms are tricky to render inline recursively; fallback:
Term::Let { name, value, body } => { Term::Let { name, value, body } => {
format!( format!(
"(let {name} {} {})", "(let {name} {} {})",
@@ -281,8 +281,8 @@ fn lit_to_string(l: &Literal) -> String {
Literal::Int { value } => value.to_string(), Literal::Int { value } => value.to_string(),
Literal::Bool { value } => value.to_string(), Literal::Bool { value } => value.to_string(),
Literal::Str { value } => { Literal::Str { value } => {
// serde_json escapt für uns; das Ergebnis ist ein gültiges // serde_json escapes for us; the result is a valid
// JSON-String-Literal, was für uns als kanonische Form ausreicht. // JSON string literal, which is enough as our canonical form.
serde_json::to_string(value).unwrap() serde_json::to_string(value).unwrap()
} }
Literal::Unit => "()".to_string(), Literal::Unit => "()".to_string(),
+36 -36
View File
@@ -1,10 +1,10 @@
//! Workspace-Loader: lädt ein Eintrittsmodul und folgt rekursiv dessen //! Workspace loader: loads an entry module and recursively follows its
//! `imports`. Konvention: ein `import { module: "foo" }` wird relativ zum //! `imports`. Convention: an `import { module: "foo" }` is resolved
//! Verzeichnis des Eintrittsfiles als `<root_dir>/foo.ail.json` aufgelöst. //! relative to the entry file's directory as `<root_dir>/foo.ail.json`.
//! //!
//! Iter 5a hat die Verantwortung, alle erreichbaren Module zu **finden** und //! Iter 5a is responsible for **finding** and consistently **loading**
//! konsistent zu **laden**. Cross-Modul-Typcheck (5b) und -Codegen (5c) bauen //! all reachable modules. Cross-module typecheck (5b) and codegen (5c)
//! darauf auf, sind aber nicht Teil dieses Schritts. //! build on top, but are not part of this step.
use crate::ast::Module; use crate::ast::Module;
use crate::canonical; use crate::canonical;
@@ -12,12 +12,12 @@ use crate::{load_module, Error as CoreError};
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
/// Vollständig geladener Workspace. /// Fully loaded workspace.
/// ///
/// `entry` benennt das Eintrittsmodul (Modulname, **nicht** Pfad). Alle /// `entry` names the entry module (module name, **not** a path). All
/// transitiv erreichbaren Module sind in `modules` enthalten und per /// transitively reachable modules are contained in `modules` and indexed
/// `Module.name` indiziert. `root_dir` ist das Verzeichnis, in dem das /// by `Module.name`. `root_dir` is the directory the entry file lives
/// Eintrittsfile liegt; alle Imports werden relativ dazu aufgelöst. /// in; all imports are resolved relative to it.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Workspace { pub struct Workspace {
pub entry: String, pub entry: String,
@@ -25,10 +25,10 @@ pub struct Workspace {
pub root_dir: PathBuf, pub root_dir: PathBuf,
} }
/// Strukturierte Fehler des Workspace-Loaders. /// Structured errors of the workspace loader.
/// ///
/// `Cycle.path` ist die Kette der Modul-Namen, in der der Zyklus geschlossen /// `Cycle.path` is the chain of module names in which the cycle was
/// wurde — das letzte Element ist der bereits in `visiting` enthaltene Name. /// closed — the last element is the name already present in `visiting`.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum WorkspaceLoadError { pub enum WorkspaceLoadError {
#[error("io error for {path}: {source}")] #[error("io error for {path}: {source}")]
@@ -65,24 +65,24 @@ pub enum WorkspaceLoadError {
ModuleHashMismatch { name: String }, ModuleHashMismatch { name: String },
} }
/// Hash über die kanonischen Bytes eines kompletten Moduls. /// Hash over the canonical bytes of a complete module.
/// ///
/// Parallel zu `def_hash`, aber auf Modul-Ebene. Der Workspace-Loader nutzt /// Parallel to `def_hash`, but at module level. The workspace loader
/// das, um Doppelladungen zu verifizieren; die CLI nutzt das, um pro Modul /// uses this to verify double-loads; the CLI uses it to emit a stable
/// einen stabilen Identifier auszugeben. /// per-module identifier.
pub fn module_hash(m: &Module) -> String { pub fn module_hash(m: &Module) -> String {
let bytes = canonical::to_bytes(m); let bytes = canonical::to_bytes(m);
let h = blake3::hash(&bytes); let h = blake3::hash(&bytes);
h.to_hex().as_str()[..16].to_string() h.to_hex().as_str()[..16].to_string()
} }
/// Lade Eintrittsmodul plus alle transitiv erreichbaren Module. /// Load entry module plus all transitively reachable modules.
/// ///
/// Algorithmus: DFS über `imports`, mit zwei Mengen: /// Algorithm: DFS over `imports`, with two sets:
/// - `loaded` (= `modules`-Map): Module, deren Subtree bereits vollständig /// - `loaded` (= `modules` map): modules whose subtree is already fully
/// abgearbeitet ist. Beim erneuten Treffen wird nur Hash-Konsistenz geprüft. /// processed. On a re-hit only hash consistency is checked.
/// - `visiting`: Stack von Modulen, deren DFS-Abstieg noch läuft. Treffer /// - `visiting`: stack of modules whose DFS descent is still running.
/// hier = Zyklus. /// A hit here = cycle.
pub fn load_workspace(entry_path: &Path) -> Result<Workspace, WorkspaceLoadError> { pub fn load_workspace(entry_path: &Path) -> Result<Workspace, WorkspaceLoadError> {
let entry_path = entry_path.to_path_buf(); let entry_path = entry_path.to_path_buf();
let root_dir = entry_path let root_dir = entry_path
@@ -90,7 +90,7 @@ pub fn load_workspace(entry_path: &Path) -> Result<Workspace, WorkspaceLoadError
.map(Path::to_path_buf) .map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(".")); .unwrap_or_else(|| PathBuf::from("."));
// Eintrittsmodul laden + Konventions-Check Name<->Dateiname. // Load entry module + check name<->filename convention.
let entry_module = load_one(&entry_path)?; let entry_module = load_one(&entry_path)?;
let expected_entry_name = module_name_from_path(&entry_path); let expected_entry_name = module_name_from_path(&entry_path);
if entry_module.name != expected_entry_name { if entry_module.name != expected_entry_name {
@@ -130,13 +130,13 @@ fn visit(
) -> Result<(), WorkspaceLoadError> { ) -> Result<(), WorkspaceLoadError> {
let name = module.name.clone(); let name = module.name.clone();
// Schon abgeschlossen geladen? Dann nichts tun. Hash-Konsistenz wird beim // Already fully loaded? Then do nothing. Hash consistency is checked
// Wieder-Antreffen über Imports geprüft (siehe unten in der Schleife). // on re-encounter via imports (see below in the loop).
if modules.contains_key(&name) { if modules.contains_key(&name) {
return Ok(()); return Ok(());
} }
// Zyklus: derselbe Name liegt aktuell auf dem DFS-Stack. // Cycle: same name currently on the DFS stack.
if visiting_set.contains(&name) { if visiting_set.contains(&name) {
let mut path = visiting.clone(); let mut path = visiting.clone();
path.push(name); path.push(name);
@@ -146,14 +146,14 @@ fn visit(
visiting.push(name.clone()); visiting.push(name.clone());
visiting_set.insert(name.clone()); visiting_set.insert(name.clone());
// Imports rekursiv abarbeiten. // Process imports recursively.
let imports = module.imports.clone(); let imports = module.imports.clone();
for imp in &imports { for imp in &imports {
let imp_path = root_dir.join(format!("{}.ail.json", imp.module)); let imp_path = root_dir.join(format!("{}.ail.json", imp.module));
if let Some(existing) = modules.get(&imp.module) { if let Some(existing) = modules.get(&imp.module) {
// Schon vollständig geladenHash-Konsistenz prüfen, falls die // Already fully loadedcheck hash consistency, in case the
// Datei auf Platte sich geändert hat. // file on disk has changed.
let on_disk = match load_one(&imp_path) { let on_disk = match load_one(&imp_path) {
Ok(m) => m, Ok(m) => m,
Err(WorkspaceLoadError::Io { .. }) => continue, Err(WorkspaceLoadError::Io { .. }) => continue,
@@ -213,11 +213,11 @@ fn load_one(path: &Path) -> Result<Module, WorkspaceLoadError> {
fn module_name_from_path(p: &Path) -> String { fn module_name_from_path(p: &Path) -> String {
let file = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); let file = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
// Konvention: `<name>.ail.json`. // Convention: `<name>.ail.json`.
if let Some(stripped) = file.strip_suffix(".ail.json") { if let Some(stripped) = file.strip_suffix(".ail.json") {
return stripped.to_string(); return stripped.to_string();
} }
// Fallback: nur die letzte Extension entfernen. // Fallback: strip only the last extension.
Path::new(file) Path::new(file)
.file_stem() .file_stem()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
@@ -258,9 +258,9 @@ mod tests {
#[test] #[test]
fn loads_example_workspace_happy_path() { fn loads_example_workspace_happy_path() {
// Nutzt die kanonischen Beispiel-Files unter `examples/`. Dieser // Uses the canonical example files under `examples/`. This
// Test dokumentiert, dass der Loader sich auf das committete // test documents that the loader relies on the committed
// Workspace-Beispiel verlässt. // workspace example.
let manifest_dir = env!("CARGO_MANIFEST_DIR"); let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = let workspace_root =
Path::new(manifest_dir).parent().unwrap().parent().unwrap(); Path::new(manifest_dir).parent().unwrap().parent().unwrap();
+138 -126
View File
@@ -1,136 +1,148 @@
# AILang — Designentscheidungen # AILang — design decisions
Dieses Dokument hält die Kernentscheidungen für AILang fest. Es ist mein Vertrag mit This document records the core decisions for AILang. It is my contract with
mir selbst über künftige Iterationen. Kürzungen statt Wachstum bevorzugen. myself across future iterations. Prefer cuts over growth.
## Zielsetzung ## Goal
AILang ist eine Programmiersprache für LLM-Autoren. Sie wird zu LLVM IR kompiliert. AILang is a programming language for LLM authors. It compiles to LLVM IR.
Performance: nativ, ohne GC für den MVP. Performance: native, no GC for the MVP.
Optimiert für: Optimised for:
- **Maschinenlesbarkeit** statt menschlicher Ergonomie. Quelle ist strukturiert. - **Machine readability** over human ergonomics. The source is structured.
- **Lokales Reasoning.** Jede Definition trägt ihren vollständigen Typ und ihre Effekte. - **Local reasoning.** Every definition carries its full type and effects.
- **Beweisbarkeit.** Reine Kernsprache, explizite Effekte, optionale Refinements. - **Provability.** Pure core language, explicit effects, optional refinements.
- **Robustheit gegen Halluzinationen.** Symbole sind hashbar; Tools können Existenz - **Robustness against hallucinations.** Symbols are hashable; tools can verify
verifizieren, ohne Kontextfenster zu verbrauchen. existence without spending context window.
## Projekt-Ökosystem ## Project ecosystem
AILang ist nicht nur eine Sprache, sondern ein Ökosystem. Die Sprache an sich AILang is not just a language but an ecosystem. The language on its own is
ist nur dann wertvoll, wenn das Drumherum sie für ihren Zielnutzer (LLM-Autoren) only valuable when its surroundings make it usable, checkable, and
benutzbar, prüfbar und erweiterbar macht. Das Repo enthält daher mehrere extensible for its target user (LLM authors). The repo therefore contains
gleichwertige Komponentenkeine davon ist optional, alle entwickeln sich several equally important componentsnone of them optional, all of them
parallel mit der Sprache: evolving in lockstep with the language:
- **Sprachkern** (`crates/ailang-core`, `crates/ailang-check`, - **Language core** (`crates/ailang-core`, `crates/ailang-check`,
`crates/ailang-codegen`): AST, Typsystem, Codegen. `crates/ailang-codegen`): AST, type system, codegen.
- **CLI** (`crates/ail`): Toolchain für Tooling-Konsumenten`manifest`, - **CLI** (`crates/ail`): toolchain for tooling consumers`manifest`,
`describe`, `deps`, `check`, `build`, etc., bevorzugt mit `--json` für `describe`, `deps`, `check`, `build`, etc., preferably with `--json` for
maschinelle Konsumption. machine consumption.
- **Beispiele** (`examples/`): kanonische `.ail.json`-Programme. Sie sind - **Examples** (`examples/`): canonical `.ail.json` programs. They are
Spezifikationsanker, nicht Demos — die E2E-Suite hängt an ihnen. specification anchors, not demos — the E2E suite hangs off them.
- **Agenten** (`agents/`): spezialisierte Sub-Prompts (Implementer, - **Agents** (`agents/`): specialised sub-prompts (implementer,
Architect, Tester, Debugger), die das Projekt-eigene LLM-Tooling bilden. architect, tester, debugger) that form the project's own LLM tooling.
Sie sind versionierter Bestandteil des Repos. Siehe `agents/README.md`. They are a versioned part of the repo. See `agents/README.md`.
- **Doku** (`docs/`): DESIGN.md (was und warum), JOURNAL.md (Verlauf). - **Docs** (`docs/`): DESIGN.md (what and why), JOURNAL.md (history).
- **Tests**: Unit pro Crate + E2E in `crates/ail/tests/e2e.rs`. Jeder neue - **Tests**: unit tests per crate plus E2E in `crates/ail/tests/e2e.rs`. Every
Compiler-Pfad braucht einen Test, sonst zählt das Feature nicht als fertig. new compiler path needs a test, otherwise the feature does not count as done.
Wenn die Sprache wächst, wachsen diese Komponenten mit. Neue Tools, die das When the language grows, these components grow with it. New tools that
LLM-Tooling stärken (z. B. `ail diff`, IR-Snapshot-Diffs, neue Agenten), strengthen the LLM tooling (e.g. `ail diff`, IR snapshot diffs, new agents)
gehören explizit ins Ökosystem-Inventar dieses Abschnitts und werden hier explicitly belong in the ecosystem inventory of this section and are added
ergänzt, sobald sie etabliert sind. here as soon as they are established.
## Entscheidung 1: Quelle = Daten, nicht Text ## Project language: English
Ein Modul ist ein JSON-Objekt mit einem festen Schema. Es gibt keinen Parser für All in-tree content is written in English: source code (identifiers,
Freitext. Tippfehler in Bezeichnern werden zu Hash-Lookup-Fehlern, die der Compiler comments, string literals, CLI help), design documents, the journal, agent
direkt vorschlägt zu fixen. prompts, READMEs, commit messages, and new examples. The only exception is
`CLAUDE.md`, which is the user's own instruction file. The live conversation
between user and me stays German for ergonomic reasons; everything that
lands in git is English. This keeps diffs and tooling output uniform and
matches the audience for AILang (LLM authors), for whom English is the
default.
Eine Textform existiert (`.ail`, S-Expression-artig), aber nur als bidirektionale ## Decision 1: source = data, not text
Projektion der JSON-Form. Sie ist für Menschen-Reviews und Diffs gedacht.
**Kanonisches Format:** `.ail.json` mit deterministischer Schlüsselreihenfolge. A module is a JSON object with a fixed schema. There is no parser for
free-form text. Typos in identifiers turn into hash-lookup errors that the
compiler proposes a fix for directly.
## Entscheidung 2: Content-addressed Definitionen A textual form exists (`.ail`, S-expression-like), but only as a
bidirectional projection of the JSON form. It is intended for human reviews
and diffs.
Jede Top-Level-Definition hat einen `hash`-Wert (BLAKE3 über kanonisches JSON ohne **Canonical format:** `.ail.json` with deterministic key order.
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: ## Decision 2: content-addressed definitions
- Refactoring durch Hinzufügen neuer Defs, nicht durch In-place-Änderung. Alte Every top-level definition has a `hash` value (BLAKE3 over canonical JSON
Versionen bleiben aufrufbar, bis manuell entfernt. without the `hash` field itself). References between definitions go primarily
- Caching von Typcheck-Ergebnissen und Codegen pro Hash. by name — names are for readability. The hash is the canonical identity.
- Diffs zeigen exakt, welche Def sich geändert hat.
## Entscheidung 3: Reine Kernsprache + algebraische Effekte Advantages:
Default sind totale, reine Funktionen. Effekte werden als Set im Funktionstyp - Refactoring by adding new defs, not by in-place change. Old versions stay
deklariert: `(Int) -> Int ![IO]`. Die Effektmenge ist row-polymorph callable until manually removed.
(`![IO | r]`). Im MVP sind nur die Effekte `IO` und `Diverge` (für Endlosschleifen) - Caching of typecheck results and codegen per hash.
verbaut. - Diffs show exactly which def has changed.
Dies ist die wichtigste LLM-Eigenschaft: Wenn ich eine Funktion lese, kann ich ## Decision 3: pure core language + algebraic effects
ihrer Signatur trauen, ohne den Body zu lesen.
## Entscheidung 4: Hindley-Milner + optionale Refinements The default is total, pure functions. Effects are declared as a set in the
function type: `(Int) -> Int ![IO]`. The effect set is row-polymorphic
(`![IO | r]`). In the MVP only the effects `IO` and `Diverge` (for infinite
loops) are wired up.
MVP: HM mit Let-Polymorphismus. Alle Typen sind inferierbar, müssen aber im This is the most important LLM property: when I read a function, I can trust
Top-Level immer explizit annotiert sein (für lokales Reasoning). its signature without reading the body.
Später: Refinement-Annotationen, die zu SMT escalieren. `(i: Int | i >= 0)`. Vom ## Decision 4: Hindley-Milner + optional refinements
Anfang an im AST vorgesehen, aber im MVP einfach als opake Strings durchgereicht.
## Entscheidung 5: LLVM IR als Text emittieren MVP: HM with let-polymorphism. All types are inferable, but at the top level
they must always be explicitly annotated (for local reasoning).
Statt `inkwell` oder `llvm-sys`: AILang erzeugt `.ll`-Dateien als Strings und Later: refinement annotations that escalate to SMT. `(i: Int | i >= 0)`. They
übergibt an `clang` zum Linken. are reserved in the AST from the start, but in the MVP they are simply passed
through as opaque strings.
Begründung: ## Decision 5: emit LLVM IR as text
- LLVM-IR-Textsyntax ist über Versionen weitgehend stabil. Instead of `inkwell` or `llvm-sys`: AILang produces `.ll` files as strings
- Keine Build-Abhängigkeit von einer bestimmten libllvm-Version. and hands them to `clang` for linking.
- 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 Rationale:
`clang -O2` als Standard-Pipeline.
## Mangling-Schema (Iter 5c) - The LLVM IR text syntax is largely stable across versions.
- No build dependency on a specific libllvm version.
- Generated code is trivially inspectable, which makes debugging much easier.
- An LLM can read the generated IR directly, which is harder with opaque
library calls.
Alle AILang-Funktionen werden zu `@ail_<modul>_<def>` gemangelt — auch im Trade-off: no inline optimisations through the LLVM API. We rely on
Single-Modul-Fall. Konstanten ebenfalls (`@ail_<modul>_<const>`). Globale `clang -O2` as the standard pipeline.
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) ## Mangling scheme (Iter 5c)
Cross-Module-Aufrufe nutzen **keinen** neuen AST-Knoten. Stattdessen ist ein All AILang functions are mangled to `@ail_<module>_<def>` — even in the
`Term::Var { name }` mit genau einem Punkt im Namen ein qualifizierter single-module case. Constants likewise (`@ail_<module>_<const>`). Global
Verweis: `<prefix>.<def>`. string literals carry a short hint for readability:
`@.str_<module>_<hint>_<idx>` (e.g. `@.str_sum_fmt_int_0`). The entry point
is a `define i32 @main()` trampoline (C / LLVM ABI) that calls
`@ail_<entry-module>_main()`. `source_filename` exists exactly once per
workspace and carries the entry-module name (`<entry-module>.ail`).
- `<prefix>` ist ein Import-Alias (`import { module: "X", as: "<prefix>" }`) ## Convention: qualified cross-module references (Iter 5b)
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 Cross-module calls use **no** new AST node. Instead, a `Term::Var { name }`
bisherigen Modul-Hashes bleiben bitidentisch. with exactly one dot in the name is a qualified reference: `<prefix>.<def>`.
## Datenmodell (MVP) - `<prefix>` is an import alias (`import { module: "X", as: "<prefix>" }`)
or, when imported without an alias, the module name itself.
- `<def>` is the name of a top-level definition in the target module.
- Def names MUST NOT contain a dot — the typechecker reports
`invalid-def-name` with `ctx: { "reason": "contains-dot" }`.
- The workspace loader (Iter 5a) finds all reachable modules; the
typechecker (Iter 5b, `check_workspace`) resolves dotted names through the
import map. Diagnostic codes: `unknown-module` (prefix not imported),
`unknown-import` (module found, def not).
Hash stability: no new AST node, no renamed fields — all previous module
hashes stay bit-identical.
## Data model (MVP)
### Module ### Module
@@ -145,7 +157,7 @@ bisherigen Modul-Hashes bleiben bitidentisch.
### Def ### Def
`kind ∈ { "fn", "type", "effect", "const" }`. Im MVP nur `fn` und `const`. `kind ∈ { "fn", "type", "effect", "const" }`. In the MVP only `fn` and `const`.
```jsonc ```jsonc
{ {
@@ -158,7 +170,7 @@ bisherigen Modul-Hashes bleiben bitidentisch.
} }
``` ```
### Term (Expression) ### Term (expression)
```jsonc ```jsonc
{ "t": "lit", "lit": { "kind": "int" | "bool" | "unit", "value": ... } } { "t": "lit", "lit": { "kind": "int" | "bool" | "unit", "value": ... } }
@@ -169,7 +181,7 @@ bisherigen Modul-Hashes bleiben bitidentisch.
{ "t": "do", "op": "<eff>/<op>", "args": [Term...] } { "t": "do", "op": "<eff>/<op>", "args": [Term...] }
``` ```
`do` ist im MVP nur ein direkter Aufruf eines Built-in-Effekt-Ops (kein Handler). In the MVP, `do` is only a direct call to a built-in effect op (no handler).
### Type ### Type
@@ -189,7 +201,7 @@ bisherigen Modul-Hashes bleiben bitidentisch.
├─ load + validate schema ├─ load + validate schema
├─ resolve names + assign hashes ├─ resolve names + assign hashes
├─ typecheck (HM, effect rows) ├─ typecheck (HM, effect rows)
├─ lower to MIR (SSA-ähnlich, named SSA-Werte) ├─ lower to MIR (SSA-like, named SSA values)
├─ emit LLVM IR (.ll) ├─ emit LLVM IR (.ll)
└─ clang -O2 *.ll -o binary └─ clang -O2 *.ll -o binary
``` ```
@@ -197,33 +209,33 @@ bisherigen Modul-Hashes bleiben bitidentisch.
## CLI ## CLI
``` ```
ail check <module.ail.json> — Lädt, validiert, typecheckt ail check <module.ail.json> — loads, validates, typechecks
ail manifest <module.ail.json> — Tabelle: name :: type !effects [hash] ail manifest <module.ail.json> — table: name :: type !effects [hash]
ail describe <module> <name> — Detail einer Definition ail describe <module> <name> — detail of a definition
ail render <module> — JSON → Pretty-Text ail render <module> — JSON → pretty-print
ail parse <module.ail> — Pretty-Text → JSON (für Bootstrapping) ail parse <module.ail> — pretty-print → JSON (for bootstrapping)
ail emit-ir <module> — schreibt .ll ail emit-ir <module> — writes .ll
ail build <module> — komplette Pipeline → Binary ail build <module> — full pipeline → binary
``` ```
## Verifikation und Korrektheit (über Zyklen) ## Verification and correctness (across cycles)
1. **Snapshot-Tests** für Pretty-Printer und IR-Emit. Diff macht Regressions sofort 1. **Snapshot tests** for the pretty-printer and IR emit. The diff makes
sichtbar. regressions visible immediately.
2. **Property-Tests** für Roundtrip JSON ↔ Pretty. 2. **Property tests** for the JSON ↔ pretty-print roundtrip.
3. **End-to-End-Tests** für `examples/` mit erwartetem Programmoutput. 3. **End-to-end tests** for `examples/` with expected program output.
4. **Hash-Stabilität**: Test stellt sicher, dass dieselbe Def stets denselben Hash 4. **Hash stability**: a test ensures the same def always produces the same
produziert. hash.
5. **CI-Pin** der Outputs in `tests/expected/`. 5. **CI pin** of the outputs in `tests/expected/`.
## Was MVP NICHT ist ## What the MVP is NOT
- Keine ADTs / Pattern Matching (Phase 2). - No ADTs / pattern matching (Phase 2).
- Keine Closures / höhere Funktionen (Phase 2). - No closures / higher-order functions (Phase 2).
- Keine Effekt-Handler (Phase 3). - No effect handlers (Phase 3).
- Keine Refinements / SMT (Phase 4). - No refinements / SMT (Phase 4).
- Kein Modulsystem über Imports hinaus (Phase 2). - No module system beyond imports (Phase 2).
- Keine Strings als first-class. Nur ints + bools + unit. - No first-class strings. Only ints + bools + unit.
Der MVP ist erfolgreich, wenn `examples/sum.ail.json` ein Binary erzeugt, das die The MVP is successful when `examples/sum.ail.json` produces a binary that
Summe 1..10 = 55 druckt. prints the sum 1..10 = 55.
+272 -276
View File
@@ -1,351 +1,347 @@
# JOURNAL # JOURNAL
Chronologische Notizen für mich. Nicht jede Änderung; nur Entscheidungen, Chronological notes for myself. Not every change; only decisions, obstacles,
Hindernisse, Beobachtungen, die zukünftige Iterationen brauchen. and observations that future iterations will need.
## 2026-05-07 — Tag 0 ## 2026-05-07 — Day 0
- Repo initialisiert. Auftrag in `CLAUDE.md`: LLM-native Sprache, LLVM-Backend. - Repo initialised. Assignment in `CLAUDE.md`: LLM-native language, LLVM backend.
- Designentscheidungen festgehalten in `docs/DESIGN.md`. - Design decisions captured in `docs/DESIGN.md`.
- Toolchain: `rustc 1.94`, `llvm-config 22.1.3`, `clang` vorhanden. - Toolchain: `rustc 1.94`, `llvm-config 22.1.3`, `clang` available.
- Entschieden gegen `inkwell` zugunsten LLVM-IR-Text-Emit. Begründung im DESIGN.md. - Decided against `inkwell` in favour of LLVM IR text emit. Rationale in DESIGN.md.
- Workspace-Layout: - Workspace layout:
- `crates/ailang-core` — AST, Type, Hash, JSON-Schema - `crates/ailang-core` — AST, type, hash, JSON schema
- `crates/ailang-check`Typchecker (kommt später) - `crates/ailang-check`typechecker (comes later)
- `crates/ailang-codegen`Lowering + LLVM IR Emit - `crates/ailang-codegen`lowering + LLVM IR emit
- `crates/ail` — CLI - `crates/ail` — CLI
- MVP-Ziel: `examples/sum.ail.json`Binary, das 55 druckt. **Erreicht.** - MVP goal: `examples/sum.ail.json`binary that prints 55. **Achieved.**
## 2026-05-07 — Architektur-Review nach MVP ## 2026-05-07 — architecture review after the MVP
Wieder am Pfad? Im Großen ja. Konkrete Beobachtungen: Still on track? Broadly yes. Concrete observations:
**Was hält:** **What holds:**
- JSON-AST + canonical form + content-hash sind alle Lego-Steine, auf denen - JSON AST + canonical form + content hash are all lego bricks that later
spätere Tools ohne Refactor aufsetzen können (`ail deps`, `ail diff`). tools can build on without a refactor (`ail deps`, `ail diff`).
- LLVM-IR-Text-Pipeline arbeitet wie geplant. Keine libllvm-Versionsschmerzen. - The LLVM IR text pipeline works as planned. No libllvm version pain.
- Effekt-Set ist im Typsystem von Anfang an verbaut. Erweiterbar zu row-poly, - The effect set is wired into the type system from the start. Extensible to
ohne den Kern zu ändern. row-poly without touching the core.
**Schulden, die Zinsen tragen:** **Debt that accrues interest:**
1. **`current_block_label_for_phi` ist eine Heuristik** (siehe codegen). Bei 1. **`current_block_label_for_phi` is a heuristic** (see codegen). On nested
verschachtelten `if`-Termen wird sie das falsche Block-Label zurückgeben, `if` terms it will return the wrong block label, because it scans the body
weil sie rückwärts den Body scannt. Tickend, weil keine Test-Fälle das backwards. Ticking, because no test cases trigger it yet. Must be fixed
bisher triggern. Muss als nächstes fixiert werden, bevor neue Sprachfeatures next, before new language features arrive.
dazu kommen. 2. **No typed AST.** Codegen reads the source AST directly and relies on
2. **Kein typed-AST.** Codegen liest direkt das Quell-AST und verlässt sich the typechecker having run before. Fine for the MVP; once ADTs or
darauf, dass der Typchecker vorher lief. Für MVP OK; sobald ADTs oder closures arrive, I will need a separate typed IR stage (TIR).
Closures dazukommen, brauche ich eine getrennte typisierte IR-Stufe (TIR). 3. **The `hash` field is not in the AST.** Right now we hash the def object
3. **`hash`-Feld ist nicht im AST.** Aktuell hashen wir das Def-Objekt direkt. directly. Once I serialise hashes as fields (caching), the hash will
Wenn ich Hashes später als Felder mitserialisiere (Caching), muss der Hash need to exclude that field before computation.
das Feld vor der Berechnung ausschließen.
**Plan Iteration 2 (jetzt):** **Plan iteration 2 (now):**
1. Block-Label-Tracking sauber machen, verschachteltes-if-Test. 1. Clean up block-label tracking, with a nested-if test.
2. Strings als Literal + `io/print_str`. 2. Strings as a literal + `io/print_str`.
3. Hello-world-Beispiel als zweiten E2E-Test. 3. Hello-world example as a second E2E test.
4. CLI: `--json`-Output für maschinelle Konsumenten überall, wo es passt. 4. CLI: `--json` output for machine consumers wherever it fits.
**Plan Iteration 3:** **Plan iteration 3:**
ADTs + Pattern Matching. Das ist der nächste große Sprung. Erfordert ADTs + pattern matching. That is the next big jump. Requires a typed IR
typisierte IR-Stufe (TIR), weil Pattern Matching zu Decision-Trees lowered stage (TIR), because pattern matching lowers into decision trees, which
wird, was eine andere Form ist als der AST. have a different shape from the AST.
## 2026-05-07 — Iteration 2 fertig ## 2026-05-07 — iteration 2 done
- Block-Label-Tracking robust (verschachtelte `if`s funktionieren). Test - Block-label tracking is now robust (nested `if`s work). Test
`max3_picks_largest` schützt das. `max3_picks_largest` protects it.
- Strings als `Lit::Str { value }`, Type `Str` -> LLVM `ptr`, mit - Strings as `Lit::Str { value }`, type `Str` -> LLVM `ptr`, with
`io/print_str` Effekt-Op. `examples/hello.ail.json` druckt einen String. `io/print_str` effect op. `examples/hello.ail.json` prints a string.
- CLI: `manifest --json`, `builtins --json` für Tool-Konsumenten. - CLI: `manifest --json`, `builtins --json` for tool consumers.
- `ail deps [--of NAME] [--json]` listet Aufruf-Edges. Effekt-Ops sind als - `ail deps [--of NAME] [--json]` lists call edges. Effect ops are tagged
`effect:NAME` markiert, damit ein Konsument sie filtern kann. `effect:NAME` so a consumer can filter them.
**Architektur-Check:** Keine strukturellen Abweichungen. Codegen liest noch **Architecture check:** no structural deviations. Codegen still reads the
direkt das Quell-AST (TIR-Stufe wird mit ADTs in Iteration 3 nötig). source AST directly (a TIR stage will become necessary with ADTs in
iteration 3).
## 2026-05-07 — Iteration 3 fertig: ADTs ## 2026-05-07 — iteration 3 done: ADTs
- TypeDef im AST mit Ctors. Ein Ctor hat `name` und `fields: [Type...]`. - TypeDef in the AST with ctors. A ctor has `name` and `fields: [Type...]`.
- Term::Ctor (Konstruktion) und Term::Match (Pattern Matching). - Term::Ctor (construction) and Term::Match (pattern matching).
- Patterns: `Wild`, `Var`, `Lit`, `Ctor { ctor, fields }`. Im MVP sind nested - Patterns: `Wild`, `Var`, `Lit`, `Ctor { ctor, fields }`. In the MVP,
Ctor-Patterns NICHT erlaubtSub-Patterns müssen `Var` oder `Wild` sein. nested ctor patterns are NOT allowedsub-patterns must be `Var` or
- Typchecker mit Type-Registry und `ctor_index` (ctor-name → ADT). Im Match `Wild`.
wird Exhaustiveness gegen die volle Konstruktormenge geprüft. Negativ-Test - Typechecker with a type registry and `ctor_index` (ctor name → ADT). In
schützt das. Match, exhaustiveness is checked against the full constructor set. A
- Codegen: Boxed-Heap-Layout. Pro Ctor-Anwendung `malloc(8 + 8*n)` Bytes; negative test protects this.
Tag in offset 0, Felder ab offset 8 (8-Byte-Slots, native typed - Codegen: boxed heap layout. Per ctor application, `malloc(8 + 8*n)`
load/store). Match: load tag + switch + arm-blocks + phi am Join. bytes; tag at offset 0, fields from offset 8 (8-byte slots, native typed
- `examples/list.ail.json` (Cons/Nil-Liste, sum_list über match) liefert 42. load/store). Match: load tag + switch + arm blocks + phi at the join.
- `examples/list.ail.json` (Cons/Nil list, sum_list via match) returns 42.
**Erstaunlich problemlos.** Die Architekturentscheidungen aus Tag 0 haben **Surprisingly painless.** The architecture decisions from day 0 paid off:
sich ausgezahlt: opaque ptr in LLVM 22 macht Boxed-Layout fast ohne opaque ptr in LLVM 22 makes the boxed layout almost glue-free; effect
Glue-Code möglich; das Effekt-Tracking blieb von ADTs unberührt; der tracking was untouched by ADTs; the JSON AST takes new node types
JSON-AST nimmt neue Knoten-Typen sauber auf. cleanly.
**Gewachsene Schulden:** **Debt accrued:**
1. **Codegen liest immer noch direkt den Quell-AST.** Die Versuchung war 1. **Codegen still reads the source AST directly.** The temptation to push
stark, ohne TIR weiterzumachenund hat funktioniert, weil meine on without TIR was strongand worked, because my Match restrictions
Match-Restriktionen flach sind (keine nested Patterns). Sobald nested are flat (no nested patterns). Once nested patterns arrive, decision-
Patterns kommen, braucht es ein Decision-Tree-Lowering, das ohne TIR tree lowering will not stay clean without TIR. Debt acknowledged; not
nicht sauber wird. Schulden anerkannt; nicht jetzt fällig. due now.
2. **Kein GC.** Heap leakt. Akzeptabel für Demo-Programme; muss vor jedem 2. **No GC.** The heap leaks. Acceptable for demo programs; must be
längerläufigen Programm angegangen werden. Optionen für Phase 4: addressed before any longer-running program. Options for Phase 4:
Refcount, Boehm-GC-Linkage, Region-Inference. refcount, Boehm-GC linkage, region inference.
3. **Pretty-Printer für ADT-Werte zur Laufzeit fehlt.** `io/print_int` 3. **No runtime pretty-printer for ADT values.** `io/print_int` is enough
reicht für Demos, aber ein generisches `show :: a -> Str` für ADTs for demos, but a generic `show :: a -> Str` for ADTs would be valuable.
wäre wertvoll. Erfordert dispatch über tag — machbar, aber nicht jetzt. Requires dispatch over the tag — feasible, but not now.
**Plan Iteration 4:** **Plan iteration 4:**
Die nächsten Schritte sind weniger eindeutig. Drei Kandidaten in The next steps are less obvious. Three candidates in priority order:
Prioritätsreihenfolge:
1. **Modulsystem (Imports).** Aktuell ist alles in einem Modul. Mit 1. **Module system (imports).** Right now everything is in a single module.
mehreren Modulen + Cross-Module-Hashing wird die Sprache erst With multiple modules + cross-module hashing the language only becomes
praktikabel für mehrere Defs. practical for several defs.
2. **Strukturierte Fehlerausgabe (`ail check --json`).** Damit Tools auf 2. **Structured error output (`ail check --json`).** So tools can react to
Typfehler reagieren können, ohne Text zu parsen. type errors without parsing text.
3. **Closures / höherwertige Funktionen.** Erfordert Closure-Konvertierung 3. **Closures / higher-order functions.** Requires closure conversion and
und ist ein größerer Schritt. is a bigger step.
Iteration 4 wird (1) + (2) — beides macht das LLM-Tooling stärker und Iteration 4 will be (1) + (2) — both strengthen the LLM tooling and have
hat moderates Risiko. moderate risk.
## 2026-05-07 — Workflow-Wechsel: Orchestrator + Agent-Repo ## 2026-05-07 — workflow change: orchestrator + agent repo
Auf Anregung des Auftraggebers Wechsel auf **Orchestrator-Modus**: ich At the user's suggestion, switching to **orchestrator mode**: I delegate
delegiere abgrenzte Implementierungs-Brocken an Sub-Agenten und behalte clearly bounded implementation chunks to sub-agents and keep only
nur Architektur-Entscheidungen, Reviews und Commit-Disziplin. Vier architecture decisions, reviews, and commit discipline. Four specialised
spezialisierte Agenten formuliert: implementer, architect, tester, agents drafted: implementer, architect, tester, debugger.
debugger.
**Wichtige Korrektur:** Auftraggeber hat verlangt, dass die Agenten nicht **Important correction:** the user required the agents not to be hidden in
in `.claude/agents/` versteckt liegen, sondern als sichtbarer Bestandteil `.claude/agents/`, but versioned as a visible part of the project under
des Projekts unter `agents/` versioniert werden. DESIGN.md hat einen neuen `agents/`. DESIGN.md gained a new section "Project ecosystem", which
Abschnitt "Projekt-Ökosystem" bekommen, der das festhält: AILang ist nicht records this: AILang is not just a language, but language core + CLI +
nur eine Sprache, sondern Sprachkern + CLI + Examples + Agents + Doku + examples + agents + docs + tests, all of equal weight.
Tests, alle gleichwertig.
Aufruf-Schema: System-Prompt-Body aus `agents/<name>.md` als Präfix vor Invocation scheme: the system-prompt body from `agents/<name>.md` as a
die konkrete Aufgabe + an `general-purpose`-Agent. Funktional identisch prefix before the concrete task + sent to the `general-purpose` agent.
zum Subagent-Loading aus `.claude/agents/`, aber sichtbar im Repo. Functionally identical to subagent loading from `.claude/agents/`, but
visible in the repo.
**Plan Iteration 4 (überarbeitet):** **Plan iteration 4 (revised):**
Modulsystem ist aufwändiger als erwartet (Cross-Module-Hashing, The module system is more involved than expected (cross-module hashing,
Import-Resolution). Erst die kleineren Tooling-Wins ziehen, dann das import resolution). First the smaller tooling wins, then the module system
Modulsystem als Iteration 5: as iteration 5:
1. **Strukturierte Fehlerausgabe** (`ail check --json` mit Diagnostic- 1. **Structured error output** (`ail check --json` with a Diagnostic
Struct, stabile Codes wie `unbound-var`, `type-mismatch`). struct, stable codes like `unbound-var`, `type-mismatch`).
2. **`ail diff <a> <b>`** — semantischer Modul-Diff via Hash-Vergleich 2. **`ail diff <a> <b>`** — semantic module diff via per-def hash
pro Def. comparison.
3. **IR-Snapshot-Tests**Regressions-Schutz für die Codegen-Pipeline. 3. **IR snapshot tests**regression protection for the codegen pipeline.
## 2026-05-07 — Iteration 4 fertig: LLM-Tooling-Konsolidierung ## 2026-05-07 — iteration 4 done: LLM tooling consolidation
Drei Sub-Commits, jeder von einem `ailang-implementer`-Aufruf erzeugt Three sub-commits, each produced by an `ailang-implementer` invocation
und stichprobenartig vom Orchestrator verifiziert: and spot-checked by the orchestrator:
- `93fe723` Iter 4a: `ail check --json` mit `Diagnostic`-Struct - `93fe723` Iter 4a: `ail check --json` with a `Diagnostic` struct
(`severity`, `code`, `message`, `def`, `ctx`). Stabile Codes: (`severity`, `code`, `message`, `def`, `ctx`). Stable codes:
`unbound-var`, `type-mismatch`, `arity-mismatch`, `unbound-var`, `type-mismatch`, `arity-mismatch`,
`non-exhaustive-match`, `unknown-ctor`, `unknown-ctor-in-pattern`, `non-exhaustive-match`, `unknown-ctor`, `unknown-ctor-in-pattern`,
`nested-ctor-pattern-not-allowed`, `duplicate-def`, `nested-ctor-pattern-not-allowed`, `duplicate-def`,
`unknown-effect-op`, `unknown-type`, `schema-mismatch`. API: `unknown-effect-op`, `unknown-type`, `schema-mismatch`. API:
`check_module(&Module) -> Vec<Diagnostic>`. `check_module(&Module) -> Vec<Diagnostic>`.
- `c652b12` Iter 4b: `ail diff <a> <b> [--json]` als struktureller Top- - `c652b12` Iter 4b: `ail diff <a> <b> [--json]` as a structural top-level
Level-Def-Diff via BLAKE3-Hash. Vier Kategorien (added/removed/ def diff via BLAKE3 hash. Four categories (added/removed/changed/
changed/unchanged), alphabetisch sortiert, Exit-Code 1 bei Diff. unchanged), sorted alphabetically, exit code 1 on diff.
- `74a2005` Iter 4c: IR-Snapshot-Tests in - `74a2005` Iter 4c: IR snapshot tests in
`crates/ail/tests/snapshots/{sum,max3,hello,list}.ll`. `crates/ail/tests/snapshots/{sum,max3,hello,list}.ll`.
Normalisierung von `target triple`. Update via `UPDATE_SNAPSHOTS=1 Normalisation of `target triple`. Update via `UPDATE_SNAPSHOTS=1
cargo test ir_snapshot_`. Mismatch erzeugt `.actual`-Datei. cargo test ir_snapshot_`. Mismatch produces an `.actual` file.
Test-Stand: 28 (vorher 19). 7 E2E + 4 IR-Snapshot + 9 ailang-check + 1 Test count: 28 (previously 19). 7 E2E + 4 IR snapshot + 9 ailang-check + 1
ailang-codegen + 7 ailang-core. ailang-codegen + 7 ailang-core.
**Erledigt aus dem Schulden-Register:** **Closed from the debt register:**
- Block-Tracking im Codegen ist seit Iter 2 kein Heuristik-Risiko mehr; - Block tracking in codegen has not been a heuristic risk since Iter 2;
die explizite `current_block: String`-Spur wurde durch Iter 4c jetzt the explicit `current_block: String` track is now additionally
zusätzlich von Snapshot-Tests gegen Regression geschützt. Schuld protected against regression by Iter 4c snapshot tests. Debt closed.
geschlossen.
**Neue / präzisierte Schulden:** **New / sharpened debt:**
1. `check_module` ist **single-shot**der erste Fehler bricht ab, kein 1. `check_module` is **single-shot**the first error aborts, no
Multi-Diagnose-Sammeln. Spec war so, aber das Format suggeriert multi-diagnostic gathering. The spec was that way, but the format
Vec-Semantik. Echter Multi-Diagnose-Refactor wird billiger, sobald suggests Vec semantics. A real multi-diagnostic refactor will be
TIR existiert (zentraler Fehler-Akku durch separate Stufe). Nicht cheaper once TIR exists (a central error accumulator via a separate
jetzt fällig. stage). Not due now.
2. `source_filename` im IR ist hartkodiert auf `"<module>.ail"`. Solange 2. `source_filename` in the IR is hard-coded to `"<module>.ail"`. As long
es nur ein Top-Level-Modul gibt, plattform-stabil. Mit Iter 5 as there is only one top-level module, that is platform-stable. With
(Modulsystem + Imports) wird der Pfad relevant — dort gleich beim Iter 5 (module system + imports) the path becomes relevant — keep it
Bau pfad-unabhängig halten, sonst kippen die Snapshots. path-independent at construction time, otherwise the snapshots will
tip over.
**Plan Iteration 5:** Modulsystem mit Imports. Cross-Module-Hashing, **Plan iteration 5:** module system with imports. Cross-module hashing,
Import-Auflösung, mehrere `.ail.json`-Files in einem Build. Multi- import resolution, multiple `.ail.json` files in one build. The multi-
Diagnose-Refactor erst danach. diagnostic refactor only after that.
Sub-Schritte: Sub-steps:
- **5a — Workspace-Loader.** `ailang_core::Workspace { modules: - **5a — workspace loader.** `ailang_core::Workspace { modules:
BTreeMap<String, Module> }` plus `load_workspace(entry: &Path)`, das BTreeMap<String, Module> }` plus `load_workspace(entry: &Path)`, which
`imports` rekursiv vom Eintrittsmodul folgt. Konvention: `import { follows `imports` recursively from the entry module. Convention:
module: "foo" }` löst auf `<dir>/foo.ail.json` neben dem Entry. Zyklus- `import { module: "foo" }` resolves to `<dir>/foo.ail.json` next to the
Erkennung. CLI: bestehende Subkommandos arbeiten weiter auf einzelnem entry. Cycle detection. CLI: existing subcommands keep working on a
Modul; ein neues `ail workspace <entry>` listet alle erreichbaren single module; a new `ail workspace <entry>` lists all reachable
Module mit Hash. Tests: zwei kleine Beispielmodule mit modules with hash. Tests: two small example modules with an import
Import-Beziehung; Zyklus-Test. relation; cycle test.
- **5b — Cross-Module-Typcheck.** Typchecker bekommt `&Workspace` - **5b — cross-module typecheck.** The typechecker takes `&Workspace`
statt `&Module`. Imports werden im Env als Namespace eingehängt instead of `&Module`. Imports are mounted in the env as a namespace
(`alias.def` oder bei fehlendem Alias `module.def`). Neue Diagnostic- (`alias.def` or, with no alias, `module.def`). New diagnostic codes:
Codes: `unknown-module`, `unknown-import`, `import-cycle`, `unknown-module`, `unknown-import`, `import-cycle`, `ambiguous-name`.
`ambiguous-name`. Tests pro Code. Tests per code.
- **5c — Cross-Module-Codegen.** Emitter erzeugt IR für alle Module im - **5c — cross-module codegen.** The emitter produces IR for all modules
Workspace, prefix-mangelt mit `@ail_<module>_<def>`. E2E-Test: in the workspace, prefix-mangled with `@ail_<module>_<def>`. E2E test:
Programm, das eine Funktion aus Modul B in Modul A nutzt, liefert a program that uses a function from module B in module A returns the
korrektes Ergebnis im Binary. correct result in the binary.
- **5d — Tooling-Anpassungen.** `manifest`, `describe`, `deps`, `diff` - **5d — tooling adjustments.** `manifest`, `describe`, `deps`, `diff`
bekommen einen `--workspace`-Modus (rekursiv). Single-Modus bleibt gain a `--workspace` mode (recursive). The single mode stays the
Default für ckwärtskompatibilität. default for backwards compatibility.
Während Iter 5 gleich beim Bau **`source_filename` pfad-unabhängig During Iter 5, at construction time **keep `source_filename`
halten** (nur Modulname, kein Verzeichnis-Prefix), sonst kippen die path-independent** (module name only, no directory prefix), otherwise
IR-Snapshots. the IR snapshots will tip over.
## 2026-05-07 — Iter 5b fertig: Cross-Module-Typcheck ## 2026-05-07 — Iter 5b done: cross-module typecheck
- `check_workspace(&Workspace) -> Vec<Diagnostic>` als Top-Level-API. - `check_workspace(&Workspace) -> Vec<Diagnostic>` as the top-level API.
`check_module` bleibt erhalten und hebt das Modul intern in einen `check_module` is preserved and internally lifts the module into a
Trivial-Workspace. trivial workspace.
- Konvention für qualifizierte Verweise (in DESIGN.md festgehalten): - Convention for qualified references (recorded in DESIGN.md):
`Term::Var { name }` mit genau einem Punkt = `<prefix>.<def>`. Prefix `Term::Var { name }` with exactly one dot = `<prefix>.<def>`. Prefix is
ist Import-Alias oder Modulname. Kein neuer AST-Knoten, keine an import alias or module name. No new AST node, no renamed fields ⇒
umbenannten Felder ⇒ Hashes bleiben stabil; alle `ir_snapshot_*` hashes stay stable; all `ir_snapshot_*` still green.
weiterhin grün. - Three new diagnostic codes: `unknown-module`, `unknown-import`,
- Drei neue Diagnostic-Codes: `unknown-module`, `invalid-def-name` (with `ctx.reason: "contains-dot"`).
`unknown-import`, `invalid-def-name` (mit `ctx.reason: "contains-dot"`). - CLI: `ail check <entry>` now **always** loads via `load_workspace`.
- CLI: `ail check <entry>` lädt jetzt **immer** über `load_workspace`. Workspace load failures become structured diagnostics in JSON mode with
Workspace-Lade-Fehler werden im JSON-Modus zu strukturierten codes `module-not-found`, `module-cycle`, `module-name-mismatch`,
Diagnostics mit Codes `module-not-found`, `module-cycle`, `module-hash-mismatch`, `schema-mismatch`. `ail build` and
`module-name-mismatch`, `module-hash-mismatch`, `schema-mismatch`. `ail emit-ir` stay per single module (cross-module codegen is 5c).
`ail build` und `ail emit-ir` bleiben pro Einzelmodul (Codegen - Examples: `ws_main.ail.json` now calls `ws_lib.add` (observable). New:
cross-module ist 5c). `ws_broken.ail.json` (`unknown-import`),
- Beispiele: `ws_main.ail.json` ruft jetzt `ws_lib.add` auf
(beobachtbar). Neu: `ws_broken.ail.json` (`unknown-import`),
`ws_unknown_module.ail.json` (`unknown-module`). `ws_unknown_module.ail.json` (`unknown-module`).
- Tests: 37 grün (vorher 32). 4 neue Workspace-Integrationstests in - Tests: 37 green (previously 32). 4 new workspace integration tests in
`crates/ailang-check/tests/workspace.rs`, ein neuer e2e-Test `crates/ailang-check/tests/workspace.rs`, one new e2e test
`check_workspace_resolves_import`. `check_workspace_resolves_import`.
- Schulden: Single-Shot-Diagnostics weiterhin (Multi-Diagnose nach 5c). - Debt: single-shot diagnostics still in place (multi-diagnostic after
Punkt-Konvention deckt nur einen Punkt ab — verschachtelte Modulpfade 5c). The dot convention covers exactly one dot — nested module paths
(`a.b.c`) gibt es nicht; das wäre erst mit Hierarchie-Modulen ein (`a.b.c`) do not exist; that would only be a topic with hierarchical
Thema und fällt aktuell als `unbound-var` durch. modules and currently falls through as `unbound-var`.
## 2026-05-07 — Iter 5c fertig: Cross-Module-Codegen ## 2026-05-07 — Iter 5c done: cross-module codegen
- **Mangling-Bruch (mit Bedacht).** Alle AILang-Funktionen heißen jetzt - **Mangling break (deliberate).** All AILang functions are now called
`@ail_<modul>_<def>`, auch in Single-Modul-Programmen. Die alte Form `@ail_<module>_<def>`, even in single-module programs. The old form
`@ail_<def>` ist weg. Strings/Const-Globals analog `@ail_<def>` is gone. Strings/const globals analogously
(`@.str_<modul>_<hint>_<idx>`, `@ail_<modul>_<const>`). Eintrittspunkt (`@.str_<module>_<hint>_<idx>`, `@ail_<module>_<const>`). The entry
bleibt `main` als C-ABI: ein `define i32 @main()`-Trampoline ruft point stays `main` as C ABI: a `define i32 @main()` trampoline calls
`@ail_<entry-modul>_main()`. Fehlt im Eintrittsmodul ein `@ail_<entry-module>_main()`. If the entry module has no
`main : () -> Unit !IO`, fällt der Build mit `MissingEntryMain`. `main : () -> Unit !IO`, the build fails with `MissingEntryMain`.
- **Workspace-Lowering.** Neue Top-Level-API - **Workspace lowering.** New top-level API
`ailang_codegen::lower_workspace(ws: &Workspace) -> Result<String>` `ailang_codegen::lower_workspace(ws: &Workspace) -> Result<String>`
produziert eine einzelne `.ll` für den ganzen Workspace. Module produces a single `.ll` for the whole workspace. Modules in
alphabetisch (BTreeMap-Order); Defs in AST-Reihenfolge. Cross-Module- alphabetical order (BTreeMap order); defs in AST order. Cross-module
Calls werden im Codegen über die Import-Map des aufrufenden Moduls calls are resolved in codegen via the import map of the calling module
aufgelöst — gleiche Logik wie im Typchecker, lokal dupliziert mit — same logic as in the typechecker, locally duplicated with a
Verweis (kein gemeinsames Helper-Modul, weil die Typ-Welten cross-reference (no shared helper module, because the type worlds
unterschiedlich sind: Typchecker hantiert mit `Type`, Codegen mit differ: the typechecker handles `Type`, codegen handles `FnSig` from
`FnSig` aus llvm-Typen). llvm types).
- **CLI.** `ail build` und `ail emit-ir` laden jetzt immer den Workspace - **CLI.** `ail build` and `ail emit-ir` now always load the workspace
und checken/lowern ihn vollständig. Single-Modul-Programme funktionieren and check/lower it fully. Single-module programs keep working
weiter (Trivial-Workspace mit einem Modul). `emit_ir(m)` bleibt im (trivial workspace with one module). `emit_ir(m)` stays in the codegen
Codegen-Crate als Bequemlichkeits-API erhalten und wrapt intern in crate as a convenience API and internally wraps into a trivial
einen Trivial-Workspace. workspace.
- **Snapshots regeneriert.** `sum.ll`, `max3.ll`, `hello.ll`, `list.ll` - **Snapshots regenerated.** `sum.ll`, `max3.ll`, `hello.ll`, `list.ll`
zeigen das neue Mangling. Neuer `ws_main.ll`-Snapshot dokumentiert den show the new mangling. New `ws_main.ll` snapshot documents the
Cross-Module-Build: `@ail_ws_main_main` ruft `@ail_ws_lib_add` auf. cross-module build: `@ail_ws_main_main` calls `@ail_ws_lib_add`.
- **Tests.** 40 grün (vorher 37). Neu: `workspace_build_runs_imported_fn` - **Tests.** 40 green (previously 37). New: `workspace_build_runs_imported_fn`
(e2e: druckt 5), `ir_snapshot_ws_main`, `missing_entry_main_is_error` (e2e: prints 5), `ir_snapshot_ws_main`, `missing_entry_main_is_error`
(codegen-unit). Bestehende Verhaltens-Tests (codegen unit). Existing behaviour tests
(`sum_1_to_10_is_55`, `max3_picks_largest`, `hello_world_str_lit`, (`sum_1_to_10_is_55`, `max3_picks_largest`, `hello_world_str_lit`,
`list_sum_via_match`) bleiben grün — Verhalten unverändert, nur das `list_sum_via_match`) stay green — behaviour unchanged, only the
Mangling ist neu. mangling is new.
**Schulden geschlossen:** **Debt closed:**
- **#19 (`source_filename` härten).** In der Workspace-Welt ist - **#19 (`source_filename` hardening).** In the workspace world,
`source_filename` jetzt einheitlich `<entry-modul>.ail`, einmal pro `source_filename` is now uniformly `<entry-module>.ail`, once per
Workspace. Der bisherige hartkodierte Pfad-Punkt entfällt damit. workspace. The previous hard-coded path dot is gone with it.
**Stand:** Modulsystem ist Ende-zu-Ende geschlossen — Loader + Typcheck **State:** the module system is closed end to endloader + typecheck +
+ Codegen + Build sehen den Workspace als kohärente Einheit. Multi- codegen + build see the workspace as a coherent unit. The multi-
Diagnose-Refactor und ggf. ADT-Cross-Module bleiben für später. diagnostic refactor and possibly cross-module ADTs remain for later.
## 2026-05-07 — Iter 5d fertig: Tooling auf Workspace ausgedehnt ## 2026-05-07 — Iter 5d done: tooling extended to the workspace
- `ail manifest|describe|deps|diff <entry> --workspace` arbeiten jetzt - `ail manifest|describe|deps|diff <entry> --workspace` now operate
über alle Module des Workspaces. Default ohne Flag bleibt single-modul across all modules of the workspace. The default without the flag stays
für Rückwärtskompatibilität. Manifest sortiert nach `(modul, name)`, single-module for backwards compatibility. Manifest sorts by
describe akzeptiert Punktnotation `ws_lib.add`, deps emittiert `(module, name)`, describe accepts dotted notation `ws_lib.add`, deps
`{from_module, from_def, to_module, to_def}`-Edges, diff vergleicht emits `{from_module, from_def, to_module, to_def}` edges, diff compares
workspace-übergreifend mit added/removed/changed/unchanged_modules workspace-wide with added/removed/changed/unchanged_modules and a
und nested Sub-Diff pro changed_module. nested sub-diff per changed_module.
- Refactor: `diff_def_lists` ist die einzige Quelle der 4-Kategorie- - Refactor: `diff_def_lists` is the single source of the four-category
Logik; Single- und Workspace-Diff teilen sie. logic; single and workspace diff share it.
- Tests: 44 grün (vorher 40). Neu: `manifest_workspace_lists_all_defs`, - Tests: 44 green (previously 40). New: `manifest_workspace_lists_all_defs`,
`describe_workspace_resolves_qualified_name`, `describe_workspace_resolves_qualified_name`,
`deps_workspace_includes_cross_module`, `diff_workspace_added_module`. `deps_workspace_includes_cross_module`, `diff_workspace_added_module`.
**Beobachtung (Schuld):** `deps` filtert Builtins/Locals/Funktions- **Observation (debt):** `deps` does not filter builtins/locals/function
parameter nicht. Im Workspace-Modus wird das deutlicher als im Single- parameters. In workspace mode that becomes more visible than in single
Modus — `ws_lib.add` listet Edges auf `ws_lib.+` (Builtin) und mode — `ws_lib.add` lists edges to `ws_lib.+` (builtin) and
`ws_lib.a`/`ws_lib.b` (Funktionsparameter). Bekanntes Vorbestand- `ws_lib.a`/`ws_lib.b` (function parameters). A known pre-existing
Problem aus Iter 2; Task #22 im Backlog. issue from Iter 2; Task #22 in the backlog.
## 2026-05-07 — Architektur-Review nach Iter 5 ## 2026-05-07 — architecture review after Iter 5
Architekt-Agent gerufen. Befunde: Architect agent invoked. Findings:
1. **Mangling-Konsistenz hält.** `@ail_<modul>_<def>` durchgängig in 1. **Mangling consistency holds.** `@ail_<module>_<def>` is consistent
Funktionen, Konstanten, String-Globals, Cross-Module-Calls. across functions, constants, string globals, and cross-module calls.
Trampoline korrekt. ADT-Konstruktoren sind absichtlich symbol-frei The trampoline is correct. ADT constructors are deliberately
(inline malloc). symbol-free (inline malloc).
2. **Modul-Hashes bitidentisch seit Iter 4.** Iter-5c-Snapshot- 2. **Module hashes bit-identical since Iter 4.** The Iter 5c snapshot
Regenerierung war Codegen-Output-Änderung, nicht Hash-Bruch. regeneration was a codegen-output change, not a hash break.
3. **Drift, sofort fällig:** 3. **Drift, due now:**
- DESIGN.md sagt `define i64 @main()`, der Codegen emittiert - DESIGN.md says `define i64 @main()`, codegen emits
`define i32 @main()` (siehe `sum.ll:35`). `define i32 @main()` (see `sum.ll:35`).
- Stringschema-Notation in DESIGN.md verkürzt - String-schema notation in DESIGN.md was shortened
(`@.str_<modul>_<idx>` statt `@.str_<modul>_<hint>_<idx>`). (`@.str_<module>_<idx>` instead of `@.str_<module>_<hint>_<idx>`).
4. **Schulden, die Zinsen tragen:** `deps`-Builtin-Leck (Task #22) ist 4. **Debt that accrues interest:** the `deps` builtin leak (Task #22) has
im Workspace-Modus zu einer Falschaussage geworden — vor dem become a falsehood in workspace mode — close it before the next big
nächsten großen Sprung schließen. jump.
**Plan Iteration 6 — Aufräumarbeiten:** **Plan iteration 6 — clean-up:**
1. **DESIGN.md-Drift fixen.** Mangling-Schema-Block aktualisieren, 1. **Fix DESIGN.md drift.** Update the mangling-scheme block, correct the
`@main`-Signatur korrigieren, String-Globals präzise notieren. `@main` signature, and note the string globals precisely.
2. **`deps`-Härtung (#22).** Pro Workspace eine Top-Level-Def-Tabelle 2. **`deps` hardening (#22).** Build a top-level def table per workspace;
bauen; Edges, deren Ziel kein Top-Level-Symbol ist, filtern oder filter edges whose target is not a top-level symbol, or emit them as
als separate `builtin:`/`local:`-Kategorie ausgeben. separate `builtin:`/`local:` categories. Function parameters via
Funktionsparameter via Lexikalisches Scope-Tracking aus walk_term. lexical scope tracking from walk_term.
3. **Multi-Diagnose-Refactor (#20).** `check_workspace` akkumuliert 3. **Multi-diagnostic refactor (#20).** `check_workspace` accumulates
`Vec<Diagnostic>` über alle Defs, statt beim ersten Error zu `Vec<Diagnostic>` across all defs instead of short-circuiting on the
shortcuten. Intra-Def darf weiter shortcuten — der Wert ist first error. Intra-def may still short-circuit — the value is "see
"alle defektiven Defs auf einmal sehen", nicht "alle defektiven all broken defs at once", not "see all broken sub-terms of one def".
Subterme einer Def sehen".
Reihenfolge: 1 zuerst (Doku-Trivialität), dann 2 vor 3 (deps ist eine Order: 1 first (doc triviality), then 2 before 3 (deps is a tooling-
Tooling-Wahrheit-Reparatur, Multi-Diag eine Strukturerweiterung). truth fix, multi-diag is a structural extension).