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:
+115
-22
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user