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:
@@ -47,306 +47,11 @@ pub fn def_hash(def: &Def) -> String {
|
||||
hex.as_str()[..16].to_string()
|
||||
}
|
||||
|
||||
// `#[cfg(test)] mod tests` block relocated to
|
||||
// `crates/ailang-core/tests/hash_pin.rs` in iter form-a.1 Task 5. The
|
||||
// integration-test crate has `ailang-surface` as a dev-dependency so
|
||||
// the schema-stability pins can load `.ail` fixtures via
|
||||
// `ailang_surface::load_module`, eliminating the production-source
|
||||
// dependency on `.ail.json` fixture reads.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::*;
|
||||
|
||||
fn sample_fn() -> Def {
|
||||
Def::Fn(FnDef {
|
||||
name: "add".into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![Type::int(), Type::int()],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
params: vec!["a".into(), "b".into()],
|
||||
body: Term::App {
|
||||
callee: Box::new(Term::Var { name: "+".into() }),
|
||||
args: vec![
|
||||
Term::Var { name: "a".into() },
|
||||
Term::Var { name: "b".into() },
|
||||
],
|
||||
tail: false,
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_is_stable() {
|
||||
let h1 = def_hash(&sample_fn());
|
||||
let h2 = def_hash(&sample_fn());
|
||||
assert_eq!(h1, h2);
|
||||
assert_eq!(h1.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_changes_with_content() {
|
||||
let mut def = sample_fn();
|
||||
let h1 = def_hash(&def);
|
||||
if let Def::Fn(ref mut f) = def {
|
||||
f.name = "mul".into();
|
||||
}
|
||||
let h2 = def_hash(&def);
|
||||
assert_ne!(h1, h2);
|
||||
}
|
||||
|
||||
/// Iter 13a regression: adding `vars` to TypeDef and `args` to
|
||||
/// `Type::Con` must NOT change canonical-JSON hashes of any pre-13a
|
||||
/// definition. Recorded hashes were captured from on-disk modules
|
||||
/// before the schema extension; if this fires, a
|
||||
/// `skip_serializing_if` is missing or wrong. We deserialise the
|
||||
/// real example modules from disk to avoid drift between the test
|
||||
/// and the source-of-truth JSON.
|
||||
#[test]
|
||||
fn iter13a_schema_extension_preserves_pre_13a_hashes() {
|
||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let examples = manifest_dir.join("../../examples");
|
||||
|
||||
let sum_src = std::fs::read(examples.join("sum.ail.json"))
|
||||
.expect("examples/sum.ail.json present");
|
||||
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
|
||||
|
||||
let list_src = std::fs::read(examples.join("list.ail.json"))
|
||||
.expect("examples/list.ail.json present");
|
||||
let list_mod: crate::ast::Module = serde_json::from_slice(&list_src).unwrap();
|
||||
let int_list_def = list_mod.defs.iter().find(|d| d.name() == "IntList").unwrap();
|
||||
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
|
||||
}
|
||||
|
||||
/// Iter 19b regression: adding `suppress` to [`crate::ast::FnDef`]
|
||||
/// must NOT change canonical-JSON hashes of any pre-19b fn whose
|
||||
/// `suppress` is empty. The `skip_serializing_if = "Vec::is_empty"`
|
||||
/// predicate on the field is what enforces this; if it is wrong,
|
||||
/// every existing fixture's hash drifts and `ail diff` /
|
||||
/// `ail manifest` output breaks.
|
||||
///
|
||||
/// We construct two FnDefs that differ only in `suppress` (one
|
||||
/// empty, one missing the field). They must hash bit-identically:
|
||||
/// the canonical-JSON of both is the same byte sequence because
|
||||
/// the empty Vec is elided.
|
||||
#[test]
|
||||
fn iter19b_empty_suppress_preserves_pre_19b_hashes() {
|
||||
let with_empty_suppress = sample_fn();
|
||||
// Mutate the bare sample to set suppress explicitly to a
|
||||
// non-empty Vec, then mutate it back to empty: two distinct
|
||||
// construction paths that should still hash identically.
|
||||
let mut with_explicit_empty = sample_fn();
|
||||
if let Def::Fn(ref mut f) = with_explicit_empty {
|
||||
f.suppress = vec![];
|
||||
}
|
||||
assert_eq!(
|
||||
def_hash(&with_empty_suppress),
|
||||
def_hash(&with_explicit_empty),
|
||||
"two FnDefs with empty suppress must hash identically"
|
||||
);
|
||||
|
||||
// And: a FnDef with a *non-empty* suppress hashes
|
||||
// *differently* (sanity check that the field is in fact in
|
||||
// the canonical bytes when present).
|
||||
let mut with_suppress = sample_fn();
|
||||
if let Def::Fn(ref mut f) = with_suppress {
|
||||
f.suppress = vec![crate::ast::Suppress {
|
||||
code: "over-strict-mode".into(),
|
||||
because: "test reason".into(),
|
||||
}];
|
||||
}
|
||||
assert_ne!(
|
||||
def_hash(&with_empty_suppress),
|
||||
def_hash(&with_suppress),
|
||||
"non-empty suppress must produce a distinct hash"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 19b: an on-disk pre-19b fixture must still load cleanly
|
||||
/// (no `suppress` field present in the JSON) and produce its
|
||||
/// canonical-byte hash unchanged. The hash for `sum.sum` was
|
||||
/// recorded by the 13a regression and must stay 16 hex chars
|
||||
/// equal to `db33f57cb329935e` — this test re-asserts it after
|
||||
/// the 19b schema bump.
|
||||
#[test]
|
||||
fn iter19b_schema_extension_preserves_pre_19b_hashes() {
|
||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let examples = manifest_dir.join("../../examples");
|
||||
|
||||
let sum_src = std::fs::read(examples.join("sum.ail.json"))
|
||||
.expect("examples/sum.ail.json present");
|
||||
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
|
||||
}
|
||||
|
||||
/// Iter 22b.1 regression: adding `Def::Class` and `Def::Instance`
|
||||
/// must NOT change canonical-JSON hashes of any pre-22b def. The
|
||||
/// new variants are alternatives, not field additions — pre-22b
|
||||
/// fixtures simply do not produce a `Class` / `Instance` `Def`,
|
||||
/// and the existing `Fn` / `Const` / `Type` arms are unchanged.
|
||||
/// This re-asserts the same on-disk hashes the 13a / 19b
|
||||
/// regressions pinned, after the 22b.1 schema bump.
|
||||
#[test]
|
||||
fn iter22b1_schema_extension_preserves_pre_22b_hashes() {
|
||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let examples = manifest_dir.join("../../examples");
|
||||
|
||||
let sum_src = std::fs::read(examples.join("sum.ail.json"))
|
||||
.expect("examples/sum.ail.json present");
|
||||
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
|
||||
|
||||
let list_src = std::fs::read(examples.join("list.ail.json"))
|
||||
.expect("examples/list.ail.json present");
|
||||
let list_mod: crate::ast::Module = serde_json::from_slice(&list_src).unwrap();
|
||||
let int_list_def = list_mod
|
||||
.defs
|
||||
.iter()
|
||||
.find(|d| d.name() == "IntList")
|
||||
.unwrap();
|
||||
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
|
||||
}
|
||||
|
||||
/// Iter 22b.1: a [`crate::ast::ClassDef`] with no doc, no
|
||||
/// superclass, and an empty methods list serialises to a stable
|
||||
/// canonical form. Two construction paths (default-elided
|
||||
/// optionals vs. JSON without those keys at all) hash
|
||||
/// bit-identically. If FAIL: a `skip_serializing_if` is missing
|
||||
/// on `superclass` or `doc`, or the parser produces different
|
||||
/// canonical bytes from the same logical value.
|
||||
#[test]
|
||||
fn iter22b1_classdef_empty_optionals_hash_stable() {
|
||||
let bare = Def::Class(crate::ast::ClassDef {
|
||||
name: "Empty".into(),
|
||||
param: "a".into(),
|
||||
superclass: None,
|
||||
methods: vec![],
|
||||
doc: None,
|
||||
});
|
||||
|
||||
let json = r#"{"kind":"class","name":"Empty","param":"a","methods":[]}"#;
|
||||
let parsed: Def = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
def_hash(&bare),
|
||||
def_hash(&parsed),
|
||||
"ClassDef with elided optionals must hash identically to construction with explicit None"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 22b.2: adding `constraints` to [`crate::ast::Type::Forall`]
|
||||
/// must NOT alter canonical-JSON bytes of any pre-22b.2 polymorphic
|
||||
/// type. The `#[serde(default, skip_serializing_if = "Vec::is_empty")]`
|
||||
/// attribute on the field is what enforces this; if it is wrong,
|
||||
/// every existing fixture's hash drifts and workspace dedup keys
|
||||
/// based on the canonical bytes break.
|
||||
///
|
||||
/// Property: a `Type::Forall` constructed without specifying
|
||||
/// `constraints` (i.e. empty vec) must serialise with no
|
||||
/// `constraints` key in the canonical JSON.
|
||||
#[test]
|
||||
fn forall_without_constraints_hashes_bit_identical_to_pre_22b2() {
|
||||
use crate::ast::Type;
|
||||
let t = Type::Forall {
|
||||
vars: vec!["a".into()],
|
||||
constraints: vec![],
|
||||
body: Box::new(Type::fn_implicit(
|
||||
vec![Type::Var { name: "a".into() }],
|
||||
Type::Var { name: "a".into() },
|
||||
vec![],
|
||||
)),
|
||||
};
|
||||
let bytes = crate::canonical::to_bytes(&t);
|
||||
let s = std::str::from_utf8(&bytes).unwrap();
|
||||
assert!(
|
||||
!s.contains("constraints"),
|
||||
"Type::Forall serialised must omit `constraints` when empty; got: {s}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter ct.4 (canonical-type-names milestone close): pin the
|
||||
/// canonical-form hashes of the two cross-module fixtures
|
||||
/// migrated by `ail migrate-canonical-types` in ct.1. These
|
||||
/// hashes are the post-migration state; pre-migration values
|
||||
/// (when both fixtures carried bare cross-module Type::Con)
|
||||
/// are no longer reproducible because the ct.1 validator
|
||||
/// rejects that shape upstream.
|
||||
#[test]
|
||||
fn ct4_migrated_fixtures_have_canonical_form_hashes() {
|
||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let examples = manifest_dir.join("../../examples");
|
||||
|
||||
let ord_src = std::fs::read(examples.join("ordering_match.ail.json"))
|
||||
.expect("examples/ordering_match.ail.json present");
|
||||
let ord_mod: crate::ast::Module = serde_json::from_slice(&ord_src).unwrap();
|
||||
let main_def = ord_mod.defs.iter().find(|d| d.name() == "main").unwrap();
|
||||
assert_eq!(
|
||||
def_hash(main_def),
|
||||
"8d17235aa3d2e127",
|
||||
"ordering_match::main canonical hash must match captured post-migration value"
|
||||
);
|
||||
|
||||
let dup_a_src = std::fs::read(examples.join("test_22b1_dup_a.ail.json"))
|
||||
.expect("examples/test_22b1_dup_a.ail.json present");
|
||||
let dup_a_mod: crate::ast::Module = serde_json::from_slice(&dup_a_src).unwrap();
|
||||
// mq.1: test_22b1_dup_a has one def post-migration — the instance
|
||||
// of `test_22b1_dup_classmod.TShow` for `test_22b1_dup_b.MyInt`.
|
||||
// The `TShow` class itself moved to `test_22b1_dup_classmod` to
|
||||
// break the dup_a ↔ dup_b cycle that the pre-mq.1 bare-class
|
||||
// shape required for cross-module duplicate-instance coverage.
|
||||
// 24.2: class renamed `Show` → `TShow` and method renamed
|
||||
// `show` → `tshow` workspace-wide so prelude.Show can ship; the
|
||||
// hash captures new bytes accordingly.
|
||||
assert_eq!(dup_a_mod.defs.len(), 1, "test_22b1_dup_a expected to have exactly 1 def (the instance)");
|
||||
// Hash captured post-24.2 TShow/tshow rename. A future
|
||||
// schema-touching change that shifts this hash without intent
|
||||
// triggers this pin.
|
||||
assert_eq!(
|
||||
def_hash(&dup_a_mod.defs[0]),
|
||||
"392c247f07de6517",
|
||||
"test_22b1_dup_a instance hash drifted; expected post-24.2 captured value"
|
||||
);
|
||||
|
||||
// Also pin the new test_22b1_dup_classmod fixture's `TShow` class
|
||||
// hash: the class moved here from dup_a verbatim (same method
|
||||
// signature), so the hash is independent of the migration but
|
||||
// does shift with the 24.2 Show→TShow / show→tshow rename.
|
||||
let dup_classmod_src = std::fs::read(examples.join("test_22b1_dup_classmod.ail.json"))
|
||||
.expect("examples/test_22b1_dup_classmod.ail.json present");
|
||||
let dup_classmod_mod: crate::ast::Module = serde_json::from_slice(&dup_classmod_src).unwrap();
|
||||
assert_eq!(dup_classmod_mod.defs.len(), 1, "test_22b1_dup_classmod expected to have exactly 1 def (class TShow)");
|
||||
assert_eq!(
|
||||
def_hash(&dup_classmod_mod.defs[0]),
|
||||
"b8bca96c2d09ed93",
|
||||
"test_22b1_dup_classmod class TShow canonical hash; post-24.2 captured value"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter ct.4: re-assert that the canonical-form tightening
|
||||
/// did NOT change hashes of intra-module-only fixtures. The
|
||||
/// existing iter-13a / iter-19b / iter-22b.1 pin tests already
|
||||
/// assert these; this one names the canonical-type-names
|
||||
/// milestone explicitly so future archaeology finds the
|
||||
/// connection.
|
||||
#[test]
|
||||
fn ct4_unmigrated_fixtures_remain_bit_identical() {
|
||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let examples = manifest_dir.join("../../examples");
|
||||
|
||||
let sum_src = std::fs::read(examples.join("sum.ail.json")).unwrap();
|
||||
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
|
||||
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||
assert_eq!(def_hash(sum_def), "db33f57cb329935e",
|
||||
"sum.sum hash drifted across canonical-form tightening — unexpected");
|
||||
|
||||
let list_src = std::fs::read(examples.join("list.ail.json")).unwrap();
|
||||
let list_mod: crate::ast::Module = serde_json::from_slice(&list_src).unwrap();
|
||||
let int_list_def = list_mod.defs.iter().find(|d| d.name() == "IntList").unwrap();
|
||||
assert_eq!(def_hash(int_list_def), "b082192bd0c99202",
|
||||
"list.IntList hash drifted across canonical-form tightening — unexpected");
|
||||
}
|
||||
}
|
||||
mod tests {}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user