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:
+103
-104
@@ -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 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<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,
|
||||
|
||||
+39
-39
@@ -1,7 +1,7 @@
|
||||
//! End-to-End-Test: lade Beispielmodul, kompiliere zu Binary, führe es aus.
|
||||
//! End-to-end test: load example module, compile to binary, run it.
|
||||
//!
|
||||
//! Dieses Test schützt die wichtigste Eigenschaft der gesamten Pipeline:
|
||||
//! AST → typecheck → LLVM IR → clang → Binary → korrekter stdout.
|
||||
//! This test guards the most important property of the entire pipeline:
|
||||
//! AST → typecheck → LLVM IR → clang → binary → correct stdout.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
@@ -11,7 +11,7 @@ fn ail_bin() -> &'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: `<root_dir>/<name>.ail.json`,
|
||||
// Modul-Name muss zum Dateinamen passen.
|
||||
// Both workspaces have an empty module `root`. B additionally
|
||||
// imports `extra`. Loader convention: `<root_dir>/<name>.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");
|
||||
|
||||
@@ -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 = "<NORMALIZED>"`
|
||||
/// - 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 = \"<NORMALIZED>\"");
|
||||
} 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");
|
||||
|
||||
Reference in New Issue
Block a user