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
+101
View File
@@ -0,0 +1,101 @@
//! Integrationstests für `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.
use ailang_check::{check_workspace, Severity};
use ailang_core::load_workspace;
use std::path::Path;
fn examples_dir() -> std::path::PathBuf {
let manifest = env!("CARGO_MANIFEST_DIR");
Path::new(manifest).parent().unwrap().parent().unwrap().join("examples")
}
#[test]
fn happy_path_resolves_qualified_import() {
// ws_main importiert ws_lib und ruft `ws_lib.add` auf — vollständig
// typisiert. Erwartet: keine Diagnostics.
let entry = examples_dir().join("ws_main.ail.json");
let ws = load_workspace(&entry).expect("load ws_main");
let diags = check_workspace(&ws);
assert!(
diags.is_empty(),
"expected no diagnostics; got: {}",
serde_json::to_string_pretty(&diags).unwrap()
);
}
#[test]
fn unknown_import_is_reported() {
// ws_broken referenziert `ws_lib.bogus` — Modul ist da, Def nicht.
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.
assert_eq!(
diags[0].ctx.get("module").and_then(|v| v.as_str()),
Some("ws_lib")
);
assert_eq!(
diags[0].ctx.get("name").and_then(|v| v.as_str()),
Some("bogus")
);
}
#[test]
fn unknown_module_prefix_is_reported() {
// ws_unknown_module hat keine Imports, referenziert aber `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);
assert_eq!(diags.len(), 1, "got: {:?}", diags);
assert!(matches!(diags[0].severity, Severity::Error));
assert_eq!(diags[0].code, "unknown-module");
assert_eq!(
diags[0].ctx.get("module").and_then(|v| v.as_str()),
Some("nope")
);
}
#[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.
use ailang_core::ast::*;
use std::collections::BTreeMap;
let m = Module {
schema: ailang_core::SCHEMA.into(),
name: "t".into(),
imports: vec![],
defs: vec![Def::Const(ConstDef {
name: "weird.name".into(),
ty: Type::int(),
value: Term::Lit {
lit: Literal::Int { value: 0 },
},
doc: None,
})],
};
let mut modules = BTreeMap::new();
modules.insert(m.name.clone(), m.clone());
let ws = ailang_core::Workspace {
entry: m.name.clone(),
modules,
root_dir: std::path::PathBuf::from("."),
};
let diags = check_workspace(&ws);
assert_eq!(diags.len(), 1, "got: {:?}", diags);
assert_eq!(diags[0].code, "invalid-def-name");
assert_eq!(
diags[0].ctx.get("reason").and_then(|v| v.as_str()),
Some("contains-dot")
);
}