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).
This commit is contained in:
@@ -6,10 +6,16 @@
|
||||
//! so the post-iter `.ail` corpus is loaded from Form A rather than the
|
||||
//! deleted `.ail.json` siblings.
|
||||
//!
|
||||
//! Carve-out tests (the seven §C4 (a) subject-matter rejections and the
|
||||
//! prelude embed) stay in-place in `workspace.rs` because their
|
||||
//! `.ail.json` fixtures are not migrated; this file covers the
|
||||
//! non-carve-out cohort only.
|
||||
//! 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;
|
||||
@@ -281,3 +287,202 @@ fn mq1_xmod_constraint_class_fixture_loads() {
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user