iter form-a.1 (Tasks 1-5): additive phase + relocation

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.
This commit is contained in:
2026-05-13 11:12:48 +02:00
parent 1a065b37f1
commit 77b28ad64d
142 changed files with 2314 additions and 840 deletions
+21 -263
View File
@@ -1609,128 +1609,12 @@ mod tests {
d
}
#[test]
fn loads_example_workspace_happy_path() {
// Uses the canonical example files under `examples/`. This
// test documents that the loader relies on the committed
// workspace example.
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root =
Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace_root.join("examples").join("ws_main.ail.json");
// `loads_example_workspace_happy_path` + `loads_workspace_auto_injects_prelude`
// relocated to `crates/ailang-core/tests/workspace_pin.rs` in iter
// form-a.1 Task 5 (use `ailang_surface::load_workspace` on `.ail`
// fixtures; the original `.ail.json` fixtures are deleted in T8).
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 manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root =
Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace_root.join("examples").join("ws_main.ail.json");
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,
crate::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,
crate::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 {
crate::ast::Def::Instance(i) if i.class == "Eq" => {
if let crate::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). Ord
// extends Eq (Decision 11 single-superclass closure), so its
// shape is the same as Eq's instance set; the loader uses the
// superclass walk to verify the Eq instances exist.
assert!(
prelude.defs.iter().any(|d| matches!(
d,
crate::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 {
crate::ast::Def::Instance(i) if i.class == "Ord" => {
if let crate::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:?}"
);
}
// (See workspace_pin.rs relocation marker above.)
#[test]
fn user_module_named_prelude_is_rejected() {
@@ -1793,25 +1677,8 @@ mod tests {
/// auto-loaded prelude (iter 23.2) the registry is no longer
/// strictly empty, so the invariant is now "no entries whose
/// `defining_module` is anything other than `prelude`".
#[test]
fn iter22b1_workspace_with_no_classes_has_empty_registry() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root =
Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace_root.join("examples").join("sum.ail.json");
let ws = load_workspace(&entry).expect("sum.ail.json loads");
assert!(
ws.registry.entries.values().all(|e| 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<_>>()
);
}
// `iter22b1_workspace_with_no_classes_has_empty_registry` relocated
// to `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
fn examples_dir() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -1828,23 +1695,8 @@ mod tests {
/// fixture itself. With the auto-loaded prelude (iter 23.2)
/// the registry also contains the prelude's own entries; the
/// invariant is filtered to fixture-only entries.
#[test]
fn iter22b1_instance_in_class_module_loads_clean() {
let entry = examples_dir().join("test_22b1_orphan_class.ail.json");
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");
}
// `iter22b1_instance_in_class_module_loads_clean` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
/// Iter 22b.1: an instance declared in a module that is neither
/// the class's module nor the type's module fires `OrphanInstance`.
@@ -1853,26 +1705,8 @@ mod tests {
/// TShow Int` itself — but the entry is not the class's module
/// and `Int` is primitive, so neither leg of coherence is
/// satisfied. (24.2: class renamed `Show` → `TShow`.)
#[test]
fn iter22b1_orphan_instance_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_orphan_third.ail.json");
let err = load_workspace(&entry).expect_err("must fire orphan");
match err {
WorkspaceLoadError::OrphanInstance {
class,
type_repr,
defining_module,
..
} => {
// mq.1: `class` carries the canonical form (qualified
// for cross-module class refs).
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:?}"),
}
}
// `iter22b1_orphan_instance_fires_diagnostic` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
/// Iter 22b.1 / mq.1: two instances of the same `(class, type)`
/// pair collide on the registry's uniqueness check.
@@ -1890,29 +1724,8 @@ mod tests {
/// instances in `test_22b1_dup_same_module` — both class-leg
/// coherent, both collide on `(test_22b1_dup_same_module.TShow,
/// type_hash(Int))`. (24.2: class renamed `Show` → `TShow`.)
#[test]
fn iter22b1_duplicate_instance_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_dup_same_module.ail.json");
let err = load_workspace(&entry).expect_err("must fire duplicate");
match err {
WorkspaceLoadError::DuplicateInstance {
class,
type_repr,
first_module,
second_module,
} => {
// `class` is the on-disk `inst.class` value; the
// fixture is intra-module so it stays bare.
assert_eq!(class, "TShow");
assert_eq!(type_repr, "Int");
// Both instances live in the same module post-mq.1,
// by structural necessity — see test doc-comment.
assert_eq!(first_module, "test_22b1_dup_same_module");
assert_eq!(second_module, "test_22b1_dup_same_module");
}
other => panic!("expected DuplicateInstance, got {other:?}"),
}
}
// `iter22b1_duplicate_instance_fires_diagnostic` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
/// Iter 22b.1: an instance that omits a required (non-default)
/// method of its class fires `MissingMethod`. The fixture's
@@ -1921,23 +1734,8 @@ mod tests {
/// (Class is `TEq` (and method `tne`) rather than `Eq` / `ne` to
/// avoid colliding with the auto-loaded prelude's `class Eq` and
/// — since iter 23.5 — the prelude's polymorphic free fn `ne`.)
#[test]
fn iter22b1_missing_method_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_missing_method.ail.json");
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:?}"),
}
}
// `iter22b1_missing_method_fires_diagnostic` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
#[test]
fn class_param_in_applied_position_fires_canonical_form_rejection() {
@@ -1992,24 +1790,8 @@ mod tests {
/// at the instance site.
/// (Class is `TEq` rather than `Eq` to avoid colliding with the
/// auto-loaded prelude's `class Eq`.)
#[test]
fn instance_overriding_nonexistent_method_fires() {
let entry = std::path::PathBuf::from(
"../../examples/test_22b2_overriding_nonexistent.ail.json",
);
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:?}"),
}
}
// `instance_overriding_nonexistent_method_fires` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
/// Iter 22b.2: a class method's `Type::Forall` whose `constraints`
/// reference a type variable that is neither bound by `Forall.vars`
@@ -2054,27 +1836,8 @@ mod tests {
/// model requires `instance S T` whenever `instance C T` exists.
/// (Classes are `TEq` / `TOrd` rather than `Eq` / `Ord` to avoid
/// colliding with the auto-loaded prelude's `class Eq`.)
#[test]
fn instance_without_superclass_instance_fires() {
let entry = examples_dir().join("test_22b2_missing_superclass_instance.ail.json");
let err = load_workspace(&entry)
.expect_err("must fire missing-superclass-instance");
match err {
WorkspaceLoadError::MissingSuperclassInstance {
class, superclass, type_repr,
} => {
// mq.1: `class` is the qualified registry-key class
// name (the chain walker starts from the registry's
// qualified key); `superclass` is the canonical-form
// value from `SuperclassRef.class` (intra-module here
// so it stays bare).
assert_eq!(class, "test_22b2_missing_superclass_instance.TOrd");
assert_eq!(superclass, "TEq");
assert_eq!(type_repr, "Int");
}
other => panic!("expected MissingSuperclassInstance, got {other:?}"),
}
}
// `instance_without_superclass_instance_fires` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
fn module_with_type_def(name: &str, type_name: &str) -> Module {
serde_json::from_value(serde_json::json!({
@@ -2737,13 +2500,8 @@ mod tests {
/// cross-module type-ref fixture (`ct1_validator_accepts_qualified_xmod_ref`
/// in-test sibling). Guards the full load → validator → registry
/// path on a real on-disk pair.
#[test]
fn mq1_xmod_constraint_class_fixture_loads() {
let entry = examples_dir().join("mq1_xmod_constraint_class.ail.json");
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"));
}
// `mq1_xmod_constraint_class_fixture_loads` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
/// ext-cli.1 Task 1: the new `load_workspace_with` injection point
/// invokes the caller-supplied loader exactly once for the entry