Translate project content to English

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 12:17:48 +02:00
parent b1dbafc6f2
commit 7577ab8a90
21 changed files with 992 additions and 984 deletions
+103 -104
View File
@@ -1,8 +1,8 @@
//! `ail` — CLI für AILang.
//! `ail` — CLI for AILang.
//!
//! Subcommands sind so geschnitten, dass jedes einzelne Tool dem LLM einen
//! 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 (`<modul>.<def>`) 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 (`<module>.<def>`);
/// 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 (`<modul>.<def>`) sein.
/// Only for one symbol; without argument: for all. In workspace
/// mode the name may be in dot notation (`<module>.<def>`).
#[arg(long)]
of: Option<String>,
#[arg(long)]
json: bool,
/// Workspace-Modus: Edges sind cross-module-fähig
/// Workspace mode: edges are cross-module aware
/// (`<from-mod>.<from-def> -> <to-mod>.<to-def>`).
#[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<PathBuf>,
},
/// Komplette Pipeline: check + emit-ir + clang -> Binary.
/// Full pipeline: check + emit-ir + clang -> binary.
Build {
path: PathBuf,
#[arg(short, long)]
out: Option<PathBuf>,
/// Optimierung (z. B. `-O2`); default `-O0` für Debugbarkeit.
/// Optimization (e.g. `-O2`); default `-O0` for debuggability.
#[arg(long, default_value = "-O0")]
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 `<modul>.<def> :: <typ> ![effs] <hash>`.
// Text form: per entry `<module>.<def> :: <type> ![effs] <hash>`.
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
// (`<modul>.<def>`) oder einen Bare-Namen (matcht in allen
// Modulen, in denen die Def existiert).
// `--of NAME`: optional, accepts dot notation
// (`<module>.<def>`) or a bare name (matches in all
// modules where the def exists).
let of_filter: Option<(Option<String>, 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:<eff>/<op>` kodiert
// (siehe `walk_term`).
// Effect refs are encoded as `effect:<eff>/<op>`
// (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 Referenzbleibt im selben Modul.
// Local referencestays 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<ailang_check::Diagnostic> {
@@ -741,7 +741,7 @@ fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
ailang_core::Def::Fn(f) => walk_term(&f.body, &mut out),
ailang_core::Def::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<String>
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<String>
// --- 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<String>) {
match d {
ailang_core::Def::Fn(f) => {
@@ -1064,11 +1063,11 @@ fn def_summary(d: &ailang_core::Def) -> (&'static str, String, Vec<String>) {
}
}
/// Auflösung für `ail describe --workspace <name>`.
/// Resolution for `ail describe --workspace <name>`.
///
/// 1. `name` enthält genau einen Punkt → `<modul>.<def>` strikt auflösen.
/// 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 `<module>.<def>` 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 `<prefix> -> <module-name>` für ein einzelnes Modul. `<prefix>` ist
/// der Import-Alias falls gesetzt, sonst der Modulname selbst. Modulname
/// ohne Punkt.
/// Map `<prefix> -> <module-name>` for a single module. `<prefix>` 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<String, String> {
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<Strin
map
}
// --- Workspace-Diff -------------------------------------------------------
// --- Workspace diff -------------------------------------------------------
struct WorkspaceDiffReport {
workspace_a: String,