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");
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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": "<prefix>"}` (Iter 5b)
|
||||
//! - `unknown-import` — `ctx`: `{"module": "<m>", "name": "<def>"}` (Iter 5b)
|
||||
//! - `invalid-def-name` — `ctx`: `{"name": "<n>", "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<String>,
|
||||
/// 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}");
|
||||
|
||||
+106
-105
@@ -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<T> = std::result::Result<T, CheckError>;
|
||||
|
||||
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<Diagnostic> {
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert(m.name.clone(), m.clone());
|
||||
@@ -232,33 +232,33 @@ pub fn check_module(m: &Module) -> Vec<Diagnostic> {
|
||||
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
|
||||
/// `<prefix>.<def>` interpretiert; `<prefix>` 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 `<prefix>.<def>`; `<prefix>` 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<Diagnostic> {
|
||||
// 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<Diagnostic> {
|
||||
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<String, (Type, String)>,
|
||||
}
|
||||
|
||||
pub fn check(m: &Module) -> Result<CheckedModule> {
|
||||
// Trivial-Workspace: das Modul allein, ohne Cross-Module-Auflösung.
|
||||
// Trivial workspace: the module alone, without cross-module resolution.
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert(m.name.clone(), m.clone());
|
||||
let ws = Workspace {
|
||||
@@ -295,7 +295,7 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
|
||||
};
|
||||
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<CheckedModule> {
|
||||
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<BTreeMap<String, IndexMap<String, Type>>> {
|
||||
@@ -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<String, String> = 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 `<self>.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 `<self>.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<Type> = 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<String, Type>,
|
||||
pub effect_ops: IndexMap<String, builtins::EffectOpSig>,
|
||||
pub types: IndexMap<String, TypeDef>,
|
||||
/// Inverser Index: ctor-name -> Verweis auf die zugehörige ADT.
|
||||
/// Inverse index: ctor name -> reference to the owning ADT.
|
||||
pub ctor_index: IndexMap<String, CtorRef>,
|
||||
/// 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<String, String>,
|
||||
/// 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<String, IndexMap<String, Type>>,
|
||||
/// 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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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_<modul>_<def>`. Das gilt
|
||||
//! auch für Single-Modul-Programme. Die alte Form `@ail_<def>` entfällt.
|
||||
//! - Globale String-/Konstanten-Symbole sind pro Modul gemangelt:
|
||||
//! `@.str_<modul>_<idx>` bzw. `@ail_<modul>_<def>` für Konstanten-Globals.
|
||||
//! - Eintrittspunkt bleibt `main` (LLVM-/C-ABI). Der Generator emittiert
|
||||
//! `define i64 @main() { call @ail_<entry-modul>_main() ... }` als
|
||||
//! Trampoline auf das `main` des Eintrittsmoduls. Fehlt dieses, scheitert
|
||||
//! der Build mit `MissingEntryMain`.
|
||||
//! - `source_filename` taucht genau einmal am Anfang auf, mit
|
||||
//! `<entry-modul>.ail` als Wert (pro Workspace).
|
||||
//! Mangling scheme (Iter 5c):
|
||||
//! - **All** AILang functions become `@ail_<module>_<def>`. This holds
|
||||
//! even for single-module programs. The old form `@ail_<def>` is gone.
|
||||
//! - Global string/constant symbols are mangled per module:
|
||||
//! `@.str_<module>_<idx>` and `@ail_<module>_<def>` for constant globals.
|
||||
//! - The entry point remains `main` (LLVM/C ABI). The generator emits
|
||||
//! `define i64 @main() { call @ail_<entry-module>_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
|
||||
//! `<entry-module>.ail` as value (per workspace).
|
||||
|
||||
use ailang_core::ast::*;
|
||||
use ailang_core::Workspace;
|
||||
@@ -50,10 +50,10 @@ pub enum CodegenError {
|
||||
|
||||
type Result<T> = std::result::Result<T, CodegenError>;
|
||||
|
||||
/// 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<String> {
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert(m.name.clone(), m.clone());
|
||||
@@ -65,21 +65,21 @@ pub fn emit_ir(m: &Module) -> Result<String> {
|
||||
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
|
||||
/// (`<prefix>.<def>`) wird über die Import-Map des aufrufenden Moduls auf
|
||||
/// `@ail_<echtes_modul>_<def>` 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
|
||||
/// (`<prefix>.<def>`) is resolved via the calling module's import map
|
||||
/// to `@ail_<actual_module>_<def>`. Local var lookups (no dot) stay
|
||||
/// stack locals or local top-level defs of the current module.
|
||||
pub fn lower_workspace(ws: &Workspace) -> Result<String> {
|
||||
let mut header = String::new();
|
||||
let mut body = String::new();
|
||||
let mut all_strings: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
|
||||
// ^ pro Modul: Liste (global-name, content). Reihenfolge = Insert-Order.
|
||||
// ^ per module: list of (global-name, content). Order = insertion order.
|
||||
|
||||
// Pass 1: Pro Modul die Top-Level-Symboltabelle (für lokale Var-Lookups).
|
||||
// Pass 1: per-module top-level symbol table (for local var lookups).
|
||||
let mut module_user_fns: BTreeMap<String, BTreeMap<String, FnSig>> = BTreeMap::new();
|
||||
for (mname, m) in &ws.modules {
|
||||
let mut user_fns = BTreeMap::new();
|
||||
@@ -97,12 +97,12 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
|
||||
module_user_fns.insert(mname.clone(), user_fns);
|
||||
}
|
||||
|
||||
// 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<String, String> = 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<String> {
|
||||
.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<String> {
|
||||
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<String, (String, usize)>,
|
||||
/// Lokale Symboltabelle pro Funktion: name -> (ssa-name inkl `%`, llvm-typ).
|
||||
/// Local symbol table per function: name -> (ssa name incl. `%`, llvm type).
|
||||
locals: Vec<(String, String, String)>,
|
||||
/// 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<String, BTreeMap<String, FnSig>>,
|
||||
/// Import-Map des aktuellen Moduls (Alias/Modulname → echter Modulname).
|
||||
/// Import map of the current module (alias/module name → actual module name).
|
||||
import_map: BTreeMap<String, String>,
|
||||
/// 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<String, Vec<CtorInfo>>,
|
||||
/// Inverser Index: ctor-name -> (type_name, tag, field_llvm_types).
|
||||
/// Inverse index: ctor name -> (type_name, tag, field_llvm_types).
|
||||
ctor_index: BTreeMap<String, CtorRef>,
|
||||
/// 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_<name>
|
||||
// SSA argument name: %arg_<name>
|
||||
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<Option<String>>)> = Vec::new();
|
||||
let mut open_arm: Option<&Arm> = None;
|
||||
let mut open_var: Option<String> = 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_<modul>_<hint>_<idx>`.
|
||||
// Mangling per module: `.str_<module>_<hint>_<idx>`.
|
||||
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<String> {
|
||||
"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_<modul>_<def>` auch im Single-File-Fall.
|
||||
// Single module becomes a trivial workspace via `emit_ir`; the
|
||||
// mangling is `@ail_<module>_<def>` 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 {
|
||||
|
||||
@@ -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<Term>,
|
||||
},
|
||||
/// 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<Term>,
|
||||
},
|
||||
/// Pattern matching über einen Wert.
|
||||
/// Pattern matching over a value.
|
||||
Match {
|
||||
scrutinee: Box<Term>,
|
||||
arms: Vec<Arm>,
|
||||
@@ -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)]
|
||||
|
||||
@@ -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<T: serde::Serialize>(value: &T) -> Vec<u8> {
|
||||
let v = serde_json::to_value(value).expect("serializable");
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 `<root_dir>/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 `<root_dir>/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<Workspace, WorkspaceLoadError> {
|
||||
let entry_path = entry_path.to_path_buf();
|
||||
let root_dir = entry_path
|
||||
@@ -90,7 +90,7 @@ pub fn load_workspace(entry_path: &Path) -> Result<Workspace, WorkspaceLoadError
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
|
||||
// Eintrittsmodul laden + Konventions-Check Name<->Dateiname.
|
||||
// Load entry module + check name<->filename convention.
|
||||
let entry_module = load_one(&entry_path)?;
|
||||
let 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<Module, WorkspaceLoadError> {
|
||||
|
||||
fn module_name_from_path(p: &Path) -> String {
|
||||
let file = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
|
||||
// Konvention: `<name>.ail.json`.
|
||||
// Convention: `<name>.ail.json`.
|
||||
if let Some(stripped) = file.strip_suffix(".ail.json") {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user