8ac8756682
Ships RawBuf end-to-end as a consumer of the raw-buf.3 scope-qualified
intrinsic mechanism, plus the general Term::New construction sugar.
Working subset committed; the drop-CALL ratification (no-leak) is
re-carved to raw-buf.5 (see b49f57d) — the flat drop FUNCTION ships here,
its call-insertion needs a codegen resolution mechanism.
What ships (all green):
- raw_buf kernel-tier submodule (crates/ailang-kernel/src/raw_buf/):
RawBuf TypeDef (param-in {Int,Float,Bool}, ctor B a) + four
(intrinsic) ops new/get/set/size. parse_raw_buf + workspace injection
(kernel-tier auto-imported); workspace count 4 -> 5.
- 12 scope-qualified INTERCEPTS entries RawBuf_{new,get,set,size}__{Int,
Float,Bool} + emit fns: new allocs an @ailang_rc_alloc slab
(8-byte i64 size header + n*width element bytes), get/set
getelementptr+load/store at offset 8 + i*width, size loads the header.
Mechanical on the raw-buf.3 naming + bijection machinery; bijection
green (4 markers -> 12 entries).
- Term::New desugar (crates/ailang-core/src/desugar.rs): (new T <types>
<values>) -> (app T.new <values>), runs before check so the
type-scoped callee flows through the raw-buf.3 scope threading to
RawBuf_new__T; drops the NewArg::Type (element type inferred from
use). Both codegen Term::New deferral arms removed (replaced with
unreachable!). Ratified by new_stubt_builds_and_runs.
- Flat intrinsic-storage drop FUNCTION @drop_raw_buf_RawBuf (single
@ailang_rc_dec on the slab), emitted for any TypeDef whose new op is
(intrinsic)-bodied — distinguishes RawBuf (intrinsic new) from StubT
(real-body new -> generic ADT drop).
- E2E: raw_buf_int (-> 60), raw_buf_float (-> 4.0), raw_buf_bool
(-> 42), raw_buf_param_in_reject (param-not-in-restricted-set).
In-scope additions beyond the literal plan (both sound, ratified):
- qualify_workspace_term now normalises a monomorphic cross-module
type-scoped callee (StubT.new) to <home>.f; without it
new_stubt_builds_and_runs cannot build (StubT.new is monomorphic, so
the raw-buf.3 poly-mono path mints no symbol). Same-module + polymorphic
callees carved out.
- diagnostic-behaviour: (new T ..) missing-new-op now surfaces
type-scoped-member-not-found (desugar runs before check, bypassing
synth's Term::New arm); new-arg-kind-mismatch obsoleted. Two unit
tests updated to the new behaviour. param-in reject unaffected.
The 5 .ll snapshots gained @drop_raw_buf_RawBuf (+ partial) — injecting
raw_buf emits its drop fn into every program; benign, regenerated to the
final IR. (The plan's "snapshots stay green" assumption was wrong.)
Verification (orchestrator, this session): cargo test --workspace 676
passed / 0 failed / 2 ignored. raw_buf_int_e2e prints 60; bijection,
round-trip, new_stubt, float/bool/reject all green. The raw_buf_no_leak
test is NOT in this commit — it moves to raw-buf.5 with the drop-call
resolution that makes it pass (the slab currently leaks at scope close;
tracked, fixed next iter; milestone not released until close).
556 lines
22 KiB
Rust
556 lines
22 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};
|
|
|
|
// Adapter so this integration crate can call `load_modules_with` with
|
|
// the same Io/Schema mapping the in-mod test helper used.
|
|
fn load_one_adapter(path: &Path) -> Result<ailang_core::ast::Module, WorkspaceLoadError> {
|
|
match ailang_core::load_module(path) {
|
|
Ok(m) => Ok(m),
|
|
Err(ailang_core::Error::Io(e)) => Err(WorkspaceLoadError::Io {
|
|
path: path.to_path_buf(),
|
|
source: e,
|
|
}),
|
|
Err(e) => Err(WorkspaceLoadError::Schema {
|
|
path: path.to_path_buf(),
|
|
source: e,
|
|
}),
|
|
}
|
|
}
|
|
|
|
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"));
|
|
// the loader auto-injects three built-in kernel-tier modules
|
|
// (`prelude`, `kernel_stub` — see prep.3 of the
|
|
// kernel-extension-mechanics milestone — and `raw_buf`, the
|
|
// first real-payload kernel extension, raw-buf.4), so the count
|
|
// is the user's two modules plus those three.
|
|
assert_eq!(ws.modules.len(), 5);
|
|
assert!(ws.modules.contains_key("prelude"));
|
|
assert!(ws.modules.contains_key("kernel_stub"));
|
|
assert!(ws.modules.contains_key("raw_buf"));
|
|
}
|
|
|
|
#[test]
|
|
fn loads_workspace_auto_injects_prelude() {
|
|
// 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"
|
|
);
|
|
|
|
// 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:?}"
|
|
);
|
|
|
|
// 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];
|
|
// 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");
|
|
}
|
|
|
|
/// 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:?}"),
|
|
}
|
|
}
|
|
|
|
/// two instances of the same `(class, type)` pair
|
|
/// collide on the registry's uniqueness check. Post-canonical-class-form 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:?}"),
|
|
}
|
|
}
|
|
|
|
/// 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:?}"),
|
|
}
|
|
}
|
|
|
|
/// 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:?}"),
|
|
}
|
|
}
|
|
|
|
/// 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:?}"),
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
/// 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, prep.1 fixture-flip: §C4 (a) carve-out fixture
|
|
/// `test_ct1_bare_xmod_rejected.ail.json`. Pre-prep.1 the fixture used
|
|
/// bare `Ordering` and the validator caught it via prelude-as-candidate.
|
|
/// Post-prep.1 a bare in-scope type-name (`Ordering` from implicit
|
|
/// prelude) is ACCEPTED, so the fixture was switched to a name no
|
|
/// module declares (`Mystery_Type`); the rejection path still fires
|
|
/// `BareCrossModuleTypeRef` but the candidates list is empty.
|
|
#[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 Mystery_Type");
|
|
match err {
|
|
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, candidates } => {
|
|
assert_eq!(module, "test_ct1_bare_xmod_rejected");
|
|
assert_eq!(name, "Mystery_Type");
|
|
assert!(
|
|
candidates.is_empty(),
|
|
"no module declares Mystery_Type => candidates empty; got {candidates:?}"
|
|
);
|
|
}
|
|
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-canonical-class-form 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:?}"),
|
|
}
|
|
}
|
|
|
|
/// pd.1 Task 2 + pd.3 Task 1 relocation: `build_workspace` accepts a
|
|
/// pre-assembled modules map (with the caller having already injected
|
|
/// any implicit modules) and runs the three-stage validation pipeline.
|
|
/// The `implicit_imports` slice is what the diagnostic helpers consult
|
|
/// for fallback candidate suggestions. Relocated from the in-mod
|
|
/// `tests` of `ailang-core/src/workspace.rs` because the caller-side
|
|
/// prelude inject now uses `ailang_surface::parse_prelude()`, which
|
|
/// only typechecks across the dev-dep edge from the integration-test
|
|
/// crate (the lib-test crate sees two distinct `ailang-core`
|
|
/// compilations and fails to unify the `Module` types).
|
|
#[test]
|
|
fn build_workspace_accepts_assembled_modules_and_runs_validation() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
write_simple_module_json(dir.path(), "main", &[]);
|
|
let entry = dir.path().join("main.ail.json");
|
|
|
|
let (entry_name, root_dir, mut modules) =
|
|
ailang_core::workspace::load_modules_with(&entry, load_one_adapter)
|
|
.expect("load");
|
|
|
|
// Caller-side prelude inject — surface owns the production inject
|
|
// path; this test consumes it via the dev-dep edge to demonstrate
|
|
// the caller-injects pattern with the same source of truth.
|
|
let prelude = ailang_surface::parse_prelude();
|
|
modules.insert("prelude".to_string(), prelude);
|
|
|
|
let ws = ailang_core::workspace::build_workspace(
|
|
entry_name,
|
|
root_dir,
|
|
modules,
|
|
&["prelude"],
|
|
)
|
|
.expect("build");
|
|
|
|
assert_eq!(ws.entry, "main");
|
|
assert!(ws.modules.contains_key("main"));
|
|
assert!(ws.modules.contains_key("prelude"));
|
|
}
|