Files
AILang/crates/ailang-core/tests/workspace_pin.rs
T
Brummel 008d18bb18 iter pd.2: surface owns prelude embed; pd.1 shim retired
Moved PRELUDE_AIL const + parse_prelude fn to ailang-surface;
re-exported from surface's lib.rs. surface::load_workspace rewritten
3-line shim-call → 7-line composition: load_modules_with → reserved-name
check → inject parse_prelude → build_workspace(&["prelude"]).

Five deletions in core: PRELUDE_JSON, load_prelude, load_workspace_with
(the pd.1 shim), the top-level cfg(test) load_one fn, and the
cfg(test) crate::load_module import. Production literal-"prelude" count
in crates/ailang-core/src/ is now zero. Cross-form-identity preflight
PASSED at module_hash 3abe0d3fa3c11c99 — parse(prelude.ail) ≡
deserialize(prelude.ail.json), so pd.3's deletion of the JSON is safe.

Three new pin tests in ailang-surface/tests/: prelude_parse_pin (succeeds
+ min defs); prelude_module_hash_pin (cross-form-identity + literal-hex
anchor); prelude_injection_pin (inject + reservation).

Plan deviations (recorded in journal): switched the regression-pin
fixture from non-existent examples/hello.ail.json to hello.ail; relocated
10 in-mod core tests to crates/ailang-core/tests/workspace_pin.rs
because the dev-dep cycle (core ↔ surface) prevents in-mod aliasing in
the lib-test target (form-a.1 T5 precedent); preserved load_one
production-symbol-deletion by adding a test-mod-private load_one helper
in core's mod tests for the 2 pd.1-introduced tests still consuming it.

cargo test --workspace 573 green (+9 net from pd.1 baseline).
bench/cross_lang.py + bench/compile_check.py exit 0; bench/check.py
exit 1 with established noise envelope (bench_list_sum.bump_s, 13th
consecutive observation; pd.2 is workspace-loader-only, no codegen
touch — baseline pristine).
2026-05-14 12:57:27 +02:00

489 lines
19 KiB
Rust

//! Workspace-load pin tests, relocated from `ailang-core/src/workspace.rs`
//! `#[cfg(test)] mod tests` to a `tests/*` integration crate in iter
//! form-a.1 Task 5. The relocation switches each test from
//! `ailang_core::load_workspace` (the JSON-only loader) to
//! `ailang_surface::load_workspace` (the extension-dispatching superset),
//! so the post-iter `.ail` corpus is loaded from Form A rather than the
//! deleted `.ail.json` siblings.
//!
//! pd.2 extension: the second batch (10 tests at the end of this file)
//! is relocated from `ailang-core/src/workspace.rs::tests` because pd.2
//! deletes the `load_workspace_with` shim those tests depended on.
//! Their post-pd.2 successor is `ailang_surface::load_workspace`, which
//! is unreachable from `ailang-core` lib-tests due to the dev-dep cycle
//! (`ailang-core` dev-deps `ailang-surface` which depends on
//! `ailang-core` — fine for integration tests, breaks lib-tests). The
//! integration-test crate is the right home: it consumes both crates
//! against the same `ailang-core` artefact, so the cycle never
//! materialises.
use ailang_core::workspace::{Registry, RegistryEntry, WorkspaceLoadError};
use ailang_surface::load_workspace;
use std::path::{Path, PathBuf};
fn examples_dir() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
Path::new(manifest_dir).parent().unwrap().parent().unwrap().join("examples")
}
#[test]
fn loads_example_workspace_happy_path() {
let entry = examples_dir().join("ws_main.ail");
let ws = load_workspace(&entry).expect("load workspace");
assert_eq!(ws.entry, "ws_main");
assert!(ws.modules.contains_key("ws_main"));
assert!(ws.modules.contains_key("ws_lib"));
// Iter 23.1: the loader auto-injects the `prelude` module,
// so the count is the user's two modules plus prelude.
assert_eq!(ws.modules.len(), 3);
}
#[test]
fn loads_workspace_auto_injects_prelude() {
// Iter 23.1: the prelude module is implicitly part of every
// workspace, regardless of whether the user's modules import
// it. Loading any well-formed workspace must result in
// `ws.modules["prelude"]` being present with the Ordering
// type def.
let entry = examples_dir().join("ws_main.ail");
let ws = load_workspace(&entry).expect("load workspace");
assert!(
ws.modules.contains_key("prelude"),
"prelude module must be auto-injected; modules present: {:?}",
ws.modules.keys().collect::<Vec<_>>()
);
let prelude = &ws.modules["prelude"];
assert_eq!(prelude.name, "prelude");
assert!(
prelude.defs.iter().any(|d| matches!(
d,
ailang_core::ast::Def::Type(t) if t.name == "Ordering"
)),
"prelude must contain Ordering type def"
);
// Iter 23.2: prelude also ships the `Eq` class plus three
// primitive instances (Eq Int, Eq Bool, Eq Str).
assert!(
prelude.defs.iter().any(|d| matches!(
d,
ailang_core::ast::Def::Class(c) if c.name == "Eq"
)),
"prelude must contain Eq class def"
);
let eq_instance_types: Vec<&str> = prelude
.defs
.iter()
.filter_map(|d| match d {
ailang_core::ast::Def::Instance(i) if i.class == "Eq" => {
if let ailang_core::ast::Type::Con { name, .. } = &i.type_ {
Some(name.as_str())
} else {
None
}
}
_ => None,
})
.collect();
assert!(
eq_instance_types.contains(&"Int"),
"prelude must contain `instance Eq Int`; saw Eq instances on: {eq_instance_types:?}"
);
assert!(
eq_instance_types.contains(&"Bool"),
"prelude must contain `instance Eq Bool`; saw Eq instances on: {eq_instance_types:?}"
);
assert!(
eq_instance_types.contains(&"Str"),
"prelude must contain `instance Eq Str`; saw Eq instances on: {eq_instance_types:?}"
);
// Iter 23.3: prelude also ships the `Ord` class plus three
// primitive instances (Ord Int, Ord Bool, Ord Str).
assert!(
prelude.defs.iter().any(|d| matches!(
d,
ailang_core::ast::Def::Class(c) if c.name == "Ord"
)),
"prelude must contain Ord class def"
);
let ord_instance_types: Vec<&str> = prelude
.defs
.iter()
.filter_map(|d| match d {
ailang_core::ast::Def::Instance(i) if i.class == "Ord" => {
if let ailang_core::ast::Type::Con { name, .. } = &i.type_ {
Some(name.as_str())
} else {
None
}
}
_ => None,
})
.collect();
assert!(
ord_instance_types.contains(&"Int"),
"prelude must contain `instance Ord Int`; saw: {ord_instance_types:?}"
);
assert!(
ord_instance_types.contains(&"Bool"),
"prelude must contain `instance Ord Bool`; saw: {ord_instance_types:?}"
);
assert!(
ord_instance_types.contains(&"Str"),
"prelude must contain `instance Ord Str`; saw: {ord_instance_types:?}"
);
}
#[test]
fn iter22b1_workspace_with_no_classes_has_empty_registry() {
let entry = examples_dir().join("sum.ail");
let ws = load_workspace(&entry).expect("sum.ail loads");
assert!(
ws.registry.entries.values().all(|e: &RegistryEntry| e.defining_module == "prelude"),
"pre-22b fixture has no class/instance defs of its own; \
all registry entries must come from the auto-injected prelude. \
got non-prelude entries: {:?}",
ws.registry.entries.values()
.filter(|e| e.defining_module != "prelude")
.map(|e| &e.defining_module)
.collect::<Vec<_>>()
);
// Silence unused-import warning for `Registry`; the type is named
// in the test only via the field-access path above.
let _: Option<&Registry> = None;
}
#[test]
fn iter22b1_instance_in_class_module_loads_clean() {
let entry = examples_dir().join("test_22b1_orphan_class.ail");
let ws = load_workspace(&entry).expect("coherent instance loads");
let fixture_entries: Vec<_> = ws.registry.entries.iter()
.filter(|(_, e)| e.defining_module != "prelude")
.collect();
assert_eq!(fixture_entries.len(), 1);
let (key, entry) = fixture_entries[0];
// mq.1: registry key is keyed by the qualified class name.
// 24.2: class renamed `Show` → `TShow` workspace-wide.
assert_eq!(&key.0, "test_22b1_orphan_class.TShow");
assert_eq!(entry.defining_module, "test_22b1_orphan_class");
// `instance.class` carries the canonical-form on-disk value
// (bare for same-module per the canonical-form rule).
assert_eq!(entry.instance.class, "TShow");
}
/// Iter 22b.1: an instance declared in a module that is neither
/// the class's module nor the type's module fires `OrphanInstance`.
#[test]
fn iter22b1_orphan_instance_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_orphan_third.ail");
let err = load_workspace(&entry).expect_err("must fire orphan");
match err {
WorkspaceLoadError::OrphanInstance {
class,
type_repr,
defining_module,
..
} => {
assert_eq!(class, "test_22b1_orphan_third_classmod.TShow");
assert_eq!(type_repr, "Int");
assert_eq!(defining_module, "test_22b1_orphan_third");
}
other => panic!("expected OrphanInstance, got {other:?}"),
}
}
/// Iter 22b.1 / mq.1: two instances of the same `(class, type)` pair
/// collide on the registry's uniqueness check. Post-mq.1 both
/// instances must live in the class's or the type's module (see
/// fixture).
#[test]
fn iter22b1_duplicate_instance_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_dup_same_module.ail");
let err = load_workspace(&entry).expect_err("must fire duplicate");
match err {
WorkspaceLoadError::DuplicateInstance {
class,
type_repr,
first_module,
second_module,
} => {
assert_eq!(class, "TShow");
assert_eq!(type_repr, "Int");
assert_eq!(first_module, "test_22b1_dup_same_module");
assert_eq!(second_module, "test_22b1_dup_same_module");
}
other => panic!("expected DuplicateInstance, got {other:?}"),
}
}
/// Iter 22b.1: an instance that omits a required (non-default) method
/// of its class fires `MissingMethod`.
#[test]
fn iter22b1_missing_method_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_missing_method.ail");
let err = load_workspace(&entry).expect_err("must fire missing-method");
match err {
WorkspaceLoadError::MissingMethod {
class,
type_repr,
method,
} => {
assert_eq!(class, "TEq");
assert_eq!(type_repr, "Int");
assert_eq!(method, "teq");
}
other => panic!("expected MissingMethod, got {other:?}"),
}
}
/// Iter 22b.2: an instance that specifies a body for a method name the
/// class never declared must fire `OverridingNonExistentMethod`.
/// (`test_22b2_overriding_nonexistent` is NOT a §C4 carve-out — it
/// stays as a `.ail`-loadable fixture.)
#[test]
fn instance_overriding_nonexistent_method_fires() {
let entry = examples_dir().join("test_22b2_overriding_nonexistent.ail");
let err = load_workspace(&entry)
.expect_err("must fire overriding-non-existent-method");
match err {
WorkspaceLoadError::OverridingNonExistentMethod {
class, type_repr, method,
} => {
assert_eq!(class, "TEq");
assert_eq!(type_repr, "Int");
assert_eq!(method, "ne");
}
other => panic!("expected OverridingNonExistentMethod, got {other:?}"),
}
}
/// Iter 22b.2: an instance `C T` whose class `C` declares a superclass
/// `S` requires that `instance S T` also exist in the workspace.
#[test]
fn instance_without_superclass_instance_fires() {
let entry = examples_dir().join("test_22b2_missing_superclass_instance.ail");
let err = load_workspace(&entry)
.expect_err("must fire missing-superclass-instance");
match err {
WorkspaceLoadError::MissingSuperclassInstance {
class, superclass, type_repr,
} => {
assert_eq!(class, "test_22b2_missing_superclass_instance.TOrd");
assert_eq!(superclass, "TEq");
assert_eq!(type_repr, "Int");
}
other => panic!("expected MissingSuperclassInstance, got {other:?}"),
}
}
/// mq.1: positive on-disk pair where `Constraint.class` references a
/// class in an imported module via the qualified form loads cleanly.
#[test]
fn mq1_xmod_constraint_class_fixture_loads() {
let entry = examples_dir().join("mq1_xmod_constraint_class.ail");
let ws = load_workspace(&entry)
.expect("workspace must load with qualified Constraint.class");
assert!(ws.modules.contains_key("mq1_xmod_constraint_class"));
assert!(ws.modules.contains_key("mq1_xmod_constraint_class_dep"));
}
// ---------------------------------------------------------------------
// pd.2 relocations from `ailang-core/src/workspace.rs::tests` (the
// post-shim-retirement batch). Each test below was a `load_workspace_with`
// caller in the in-mod tests; post-pd.2 the shim is gone and these
// tests now go through `ailang_surface::load_workspace` (which composes
// `load_modules_with` + caller-side prelude inject + `build_workspace`).
//
// `tmp_dir` + `write_module` helpers are inlined per-test to keep each
// test self-contained at integration-test scope.
// ---------------------------------------------------------------------
fn write_simple_module_json(dir: &Path, name: &str, imports: &[&str]) -> PathBuf {
use std::fs;
let imports_json: Vec<serde_json::Value> = imports
.iter()
.map(|m| serde_json::json!({ "module": m }))
.collect();
let module = serde_json::json!({
"schema": "ailang/v0",
"name": name,
"imports": imports_json,
"defs": [],
});
let path = dir.join(format!("{name}.ail.json"));
fs::write(&path, serde_json::to_vec_pretty(&module).unwrap()).unwrap();
path
}
/// Iter 23.1 / pd.2 relocation: the loader auto-injects a module named
/// "prelude", so a user module that also claims that name would collide
/// silently. The collision is caught explicitly with `ReservedModuleName`.
#[test]
fn user_module_named_prelude_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let entry = write_simple_module_json(dir.path(), "prelude", &[]);
let err = load_workspace(&entry).expect_err("must reject user prelude");
match err {
WorkspaceLoadError::ReservedModuleName { name } => {
assert_eq!(name, "prelude");
}
other => panic!("expected ReservedModuleName, got: {other:?}"),
}
}
/// pd.2 relocation: workspace import-cycle detection.
#[test]
fn detects_import_cycle() {
let dir = tempfile::tempdir().unwrap();
write_simple_module_json(dir.path(), "a", &["b"]);
write_simple_module_json(dir.path(), "b", &["a"]);
let entry = dir.path().join("a.ail.json");
let err = load_workspace(&entry).expect_err("must error on cycle");
match err {
WorkspaceLoadError::Cycle { path } => {
assert!(path.contains(&"a".to_string()));
assert!(path.contains(&"b".to_string()));
}
other => panic!("expected Cycle, got {other:?}"),
}
}
/// pd.2 relocation: a missing imported module yields a structured
/// `ModuleNotFound` error naming the absent name + the path that was
/// searched.
#[test]
fn module_not_found_yields_structured_error() {
let dir = tempfile::tempdir().unwrap();
write_simple_module_json(dir.path(), "main", &["does_not_exist"]);
let entry = dir.path().join("main.ail.json");
let err = load_workspace(&entry).expect_err("must error on missing module");
match err {
WorkspaceLoadError::ModuleNotFound { name, expected_path } => {
assert_eq!(name, "does_not_exist");
assert!(expected_path.ends_with("does_not_exist.ail.json"));
}
other => panic!("expected ModuleNotFound, got {other:?}"),
}
}
/// pd.2 relocation: §C4 (a) carve-out fixture
/// `test_22b2_kind_mismatch.ail.json` — class param appearing in
/// applied position fires the canonical-form rejection
/// (`BareCrossModuleTypeRef`).
#[test]
fn class_param_in_applied_position_fires_canonical_form_rejection() {
let entry = examples_dir().join("test_22b2_kind_mismatch.ail.json");
let err = load_workspace(&entry)
.expect_err("must fire canonical-form rejection");
match err {
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, .. } => {
assert_eq!(module, "test_22b2_kind_mismatch");
assert_eq!(name, "f");
}
other => panic!(
"expected BareCrossModuleTypeRef (validator now fires first), got {other:?}",
),
}
}
/// pd.2 relocation: §C4 (a) carve-out fixture
/// `test_22b2_invalid_superclass_param.ail.json` — `class Ord a extends
/// Eq b` shape fires `InvalidSuperclassParam`.
#[test]
fn superclass_with_wrong_param_fires_invalid_superclass_param() {
let entry = examples_dir().join("test_22b2_invalid_superclass_param.ail.json");
let err = load_workspace(&entry)
.expect_err("must fire invalid-superclass-param");
match err {
WorkspaceLoadError::InvalidSuperclassParam {
class, superclass, expected_param, got_type,
} => {
assert_eq!(class, "Ord");
assert_eq!(superclass, "Eq");
assert_eq!(expected_param, "a");
assert_eq!(got_type, "b");
}
other => panic!("expected InvalidSuperclassParam, got {other:?}"),
}
}
/// pd.2 relocation: §C4 (a) carve-out fixture
/// `test_22b2_unbound_constraint_var.ail.json` — class method
/// `Type::Forall.constraints` referencing an unbound type var fires
/// `UnboundConstraintTypeVar`.
#[test]
fn constraint_with_unbound_var_fires_unbound_constraint_type_var() {
let entry = examples_dir().join("test_22b2_unbound_constraint_var.ail.json");
let err = load_workspace(&entry)
.expect_err("must fire constraint-references-unbound-type-var");
match err {
WorkspaceLoadError::UnboundConstraintTypeVar {
class, method, var, ..
} => {
assert_eq!(class, "Foo");
assert_eq!(method, "foo");
assert_eq!(var, "z");
}
other => panic!("expected UnboundConstraintTypeVar, got {other:?}"),
}
}
/// pd.2 relocation: §C4 (a) carve-out fixture
/// `test_ct1_bare_xmod_rejected.ail.json` — bare `Ordering` Term::Ctor
/// with no imports, validator catches it after prelude injection so the
/// candidate list contains `prelude.Ordering`.
#[test]
fn ct1_fixture_bare_xmod_rejected() {
let entry = examples_dir().join("test_ct1_bare_xmod_rejected.ail.json");
let err = load_workspace(&entry).expect_err("must reject bare Ordering");
match err {
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, candidates } => {
assert_eq!(module, "test_ct1_bare_xmod_rejected");
assert_eq!(name, "Ordering");
assert_eq!(candidates, vec!["prelude.Ordering".to_string()]);
}
other => panic!("expected BareCrossModuleTypeRef, got {other:?}"),
}
}
/// pd.2 relocation: §C4 (a) carve-out fixture
/// `test_ct1_bad_qualifier.ail.json` — qualified `Mystery.Type` with
/// `Mystery` not a known module fires `BadCrossModuleTypeRef`.
#[test]
fn ct1_fixture_bad_qualifier() {
let entry = examples_dir().join("test_ct1_bad_qualifier.ail.json");
let err = load_workspace(&entry).expect_err("must reject Mystery.Type");
match err {
WorkspaceLoadError::BadCrossModuleTypeRef { module, name } => {
assert_eq!(module, "test_ct1_bad_qualifier");
assert_eq!(name, "Mystery.Type");
}
other => panic!("expected BadCrossModuleTypeRef, got {other:?}"),
}
}
/// pd.2 relocation: §C4 (a) carve-out fixture
/// `test_ct1_qualified_class_rejected.ail.json` — declares
/// `instance prelude.Eq Int` outside the prelude AND outside Int's
/// defining module; post-mq.1 the qualified class-ref is schema-valid
/// and the downstream coherence check rejects with `OrphanInstance`.
#[test]
fn ct1_fixture_qualified_class_orphan_post_mq1() {
let entry = examples_dir().join("test_ct1_qualified_class_rejected.ail.json");
let err = load_workspace(&entry).expect_err("must reject (now as Orphan)");
match err {
WorkspaceLoadError::OrphanInstance {
class, type_repr, defining_module, ..
} => {
assert_eq!(class, "prelude.Eq");
assert_eq!(type_repr, "Int");
assert_eq!(defining_module, "test_ct1_qualified_class_rejected");
}
other => panic!("expected OrphanInstance, got {other:?}"),
}
}