77b28ad64d
First half of the form-a-default-authoring milestone-close iter (Boss-decided strategy C, big-bang). All five tasks DONE; cargo test --workspace green at every per-task boundary. T1 — Add three new tests: - parse_is_deterministic_over_every_ail_fixture (round_trip.rs) - cli_parse_then_render_then_parse_is_idempotent (roundtrip_cli.rs) - carve_out_inventory.rs (new file; #[ignore]'d until T8 deletion) T2 — Bulk-render the 99 missing examples/<stem>.ail files via `ail render`. Corpus 58 .ail (pre-iter) -> 157 .ail. Eight .ail.json carve-outs (7 §C4(a) subject-matter + 1 §C4(b) prelude) preserved. One re-loop triggered: load_workspace prefers .ail siblings since ext-cli.1, so the newly-rendered imports broke seven Group-A entries whose JSON entry-paths now resolved imports to fresh .ail. Repair: pre-emptive forward-pull of five T3 migrations + 4 transient #[ignore]s on workspace.rs mod tests (cleanly relocated in T5). T3 — 14 Group-A test files migrated from ailang_core::load_workspace to ailang_surface::load_workspace + .ail paths. Carve-out lines preserved verbatim (7 sites in typeclass_22b2.rs / typeclass_22b3.rs). T4 — 12 Group-B test files: bulk regex flip on build_and_run / build_and_run_with_alloc / build_and_run_with_rc_stats call sites (~70 e2e.rs invocations + 11 subprocess sites). Four files mis-classified Group-A as Group-B in plan recon (mono_hash_stability, prelude_free_fns, print_mono_body_shape, show_mono_synthesis); two files mis-classified Group-B as Group-A (mono_recursive_fn, mono_xmod_qualified_ref). Migrated per actual shape, not plan label. T5 — Relocated #[cfg(test)] mod tests from production source to integration test crates with ailang-surface dev-dependency: - crates/ailang-core/tests/hash_pin.rs (10 tests from hash.rs) - crates/ailang-core/tests/workspace_pin.rs (10 non-carve-out tests from workspace.rs) - crates/ailang-codegen/tests/eq_primitives_pin.rs (3 tests from codegen/src/lib.rs:3717-3799) - ailang-prose/tests/snapshot.rs migrated (helper + 8 fixtures) to .ail + ailang_surface::load_module Carve-out tests in workspace.rs mod tests preserved in-place (3× 22b2 + 3× ct1 = 6 tests). Tempdir-based loader-mechanism tests (3 sites) also preserved — they don't consume examples/. Tests: 560 passed, 0 failed, 4 ignored (was 558 + 3 T1 new - 1 transit carve_out_inventory #[ignore] = 560 active). Tasks 6-12 (bench-driver suffix flips, e2e diff-test rewrite + 4 additional raw-JSON-inspect handlers, .ail.json deletion, retiring obsolete roundtrip tests + schema_coverage corpus flip, §C3 DESIGN.md restatement, §A4 doctrine edits, WhatsNew + roadmap strike) ship in the next dispatch on this iter ID. Known debt inherited to T6-12: 4 raw-JSON-inspect tests in e2e.rs (borrow_own_demo / reuse_as_demo / render_parse_round_trip_canonical / ail_run_accepts_ail_source_with_same_stdout_as_ail_json dual-form smoke) need rewrite or #[ignore] before T8 deletion; recorded in journal Concerns + Known debt sections.
284 lines
11 KiB
Rust
284 lines
11 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.
|
|
//!
|
|
//! 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.
|
|
|
|
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"));
|
|
}
|