Iter 5b: Cross-Module-Typcheck mit qualifizierten Verweisen

Neue API check_workspace(&Workspace) -> Vec<Diagnostic>; check_module
hebt das Modul intern in einen Trivial-Workspace. Term::Var mit genau
einem Punkt im Namen ist ein qualifizierter Verweis <prefix>.<def>,
aufgelöst über Import-Map (alias-oder-modulname → echter Modulname).
Drei neue Diagnostic-Codes: unknown-module, unknown-import,
invalid-def-name. ail check lädt jetzt immer via load_workspace;
Loader-Fehler werden im JSON-Modus zu strukturierten Diagnostics
(module-not-found, module-cycle, …). DESIGN.md und JOURNAL.md
dokumentieren die Konvention. Hash-Stabilität: alle ir_snapshot_*-
Tests bitidentisch grün, kein neuer AST-Knoten.
This commit is contained in:
2026-05-07 11:31:57 +02:00
parent 3451b5bd15
commit b2878fe655
10 changed files with 601 additions and 67 deletions
+115 -22
View File
@@ -194,27 +194,23 @@ 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.
if json {
// JSON-Modus: stdout enthält ausschließlich das Diagnostics-
// Array. Schema-Mismatch wird als strukturiertes Diagnostic
// emittiert (mit Code `schema-mismatch`); echte I/O-Fehler
// propagieren als fatal, weil sie keinen Modul-bezogenen
// Diagnostic-Kontext haben.
let diags = match ailang_core::load_module(&path) {
Ok(m) => ailang_check::check_module(&m),
Err(ailang_core::Error::SchemaMismatch { expected, got }) => {
vec![ailang_check::Diagnostic::error(
"schema-mismatch",
format!(
"schema mismatch: expected {expected:?}, got {got:?}"
),
)
.with_ctx(serde_json::json!({
"expected": expected,
"actual": got,
}))]
}
Err(e) => return Err(anyhow::anyhow!(e)),
// Array. Workspace-Lade-Fehler werden als strukturierte
// Diagnostics emittiert (Codes `module-not-found`,
// `module-cycle`, `module-name-mismatch`, `schema-mismatch`).
// Echte I/O-Fehler des Eintrittsfiles bleiben fatal.
let diags = match ailang_core::load_workspace(&path) {
Ok(ws) => ailang_check::check_workspace(&ws),
Err(e) => match workspace_error_to_diagnostic(&e) {
Some(d) => vec![d],
None => return Err(anyhow::anyhow!(e)),
},
};
println!("{}", serde_json::to_string(&diags)?);
if diags
@@ -224,9 +220,32 @@ fn main() -> Result<()> {
std::process::exit(1);
}
} else {
let m = ailang_core::load_module(&path)?;
let r = ailang_check::check(&m)?;
println!("ok ({} symbols)", r.symbols.len());
let ws = ailang_core::load_workspace(&path)?;
let diags = ailang_check::check_workspace(&ws);
if !diags.is_empty() {
for d in &diags {
eprintln!(
"{}: [{}] {}{}",
match d.severity {
ailang_check::Severity::Error => "error",
ailang_check::Severity::Warning => "warning",
},
d.code,
d.def
.as_ref()
.map(|n| format!("{n}: "))
.unwrap_or_default(),
d.message,
);
}
std::process::exit(1);
}
let total: usize = ws.modules.values().map(|m| m.defs.len()).sum();
println!(
"ok ({} symbols across {} modules)",
total,
ws.modules.len()
);
}
}
Cmd::EmitIr { path, out } => {
@@ -384,6 +403,80 @@ 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.
fn workspace_error_to_diagnostic(
e: &ailang_core::WorkspaceLoadError,
) -> Option<ailang_check::Diagnostic> {
use ailang_core::WorkspaceLoadError as W;
match e {
W::Io { .. } => None,
W::Schema { source, .. } => match source {
ailang_core::Error::SchemaMismatch { expected, got } => Some(
ailang_check::Diagnostic::error(
"schema-mismatch",
format!(
"schema mismatch: expected {expected:?}, got {got:?}"
),
)
.with_ctx(serde_json::json!({
"expected": expected,
"actual": got,
})),
),
_ => None,
},
W::ModuleNotFound { name, expected_path } => Some(
ailang_check::Diagnostic::error(
"module-not-found",
format!(
"module `{name}` not found (expected at {})",
expected_path.display()
),
)
.with_ctx(serde_json::json!({
"module": name,
"expected_path": expected_path.display().to_string(),
})),
),
W::ModuleNameMismatch {
name_in_file,
name_from_path,
} => Some(
ailang_check::Diagnostic::error(
"module-name-mismatch",
format!(
"module name mismatch: file says {name_in_file:?}, path implies {name_from_path:?}"
),
)
.with_ctx(serde_json::json!({
"name_in_file": name_in_file,
"name_from_path": name_from_path,
})),
),
W::Cycle { path } => Some(
ailang_check::Diagnostic::error(
"module-cycle",
format!("import cycle: {}", path.join(" -> ")),
)
.with_ctx(serde_json::json!({
"path": path,
})),
),
W::ModuleHashMismatch { name } => Some(
ailang_check::Diagnostic::error(
"module-hash-mismatch",
format!("module `{name}` loaded twice with differing content"),
)
.with_ctx(serde_json::json!({
"module": name,
})),
),
}
}
fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
let mut out = std::collections::BTreeSet::new();
match def {