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