iter prep.1-type-scoped-namespacing (DONE 5/5): TypeDef-first resolution + workspace pre-pass — closes #31
First iteration of the kernel-extension-mechanics milestone. Ships
the type-scoped `<TypeName>.<member>` resolution path as the
canonical form for type-associated operations, narrows the
`BareCrossModuleTypeRef` / `BadCrossModuleTypeRef` diagnostics from
"bare = strictly local" to "bare = in-scope by any path", migrates
12 std-library example fixtures, and introduces a workspace-wide
normalisation pre-pass `prepare_workspace_for_check` shared between
`check_workspace` and `monomorphise_workspace`.
Architectural discovery during implementation: the plan covered the
`Term::Var` dot-qualified resolver layer plus the workspace
validator's bare-name acceptance, but the migration of bare-form
fixtures exposed five sites where bare vs. qualified type-names
needed symmetric treatment — `Term::Ctor` resolution, `Type::Con`
well-formedness, mono's poly-free-fn name/constraint-count
enumeration, codegen's `lookup_ctor_by_type` bare-name path, and
the upstream desugar-then-qualify composition. Rather than
scattering TypeDef-first ladders across each site, the implementer
centralised the work into one pre-pass that walks every consumer
module's `Type::Con.name` and `Term::Ctor.type_name`, rewriting
bare cross-module references to their qualified `<home>.<Type>`
form. This is symmetric to the pre-existing `qualify_local_types`
(owner-side); the new pre-pass is the consumer-side mirror.
Downstream passes see qualified Types regardless of authoring form.
The TypeDef-first ladder still lives in `synth`'s `Term::Var` arm
because `<TypeName>.<member>` is term-position-only — `Maybe.from_maybe`
is a Var, not a Type expression, and the pre-pass does not rewrite
Var names.
Alternatives considered:
(a) Add TypeDef-first ladder at every resolution site separately
(the plan's implicit assumption). Rejected: O(N) extension
sites, each carrying the same workspace-walking logic; the
pre-pass version is O(1) — one pass, every downstream consumer
benefits.
(b) BLOCKED + spec re-brainstorm. Rejected: the architecture
extension is consistent with prep.1's thesis (bare type-name
resolves to the workspace-wide TypeDef) and forward-compatible
with prep.2 (Term::New.type_name falls under the same rewrite)
and prep.3 (kernel-tier TypeDefs enter the workspace map
automatically). No design regression to bounce back over.
Spec updated to document the realisation mechanism honestly: the
"Realisation mechanism — workspace pre-pass" subsection clarifies
that the resolver-level semantics described in "Implementation
shape" are the user-facing contract, and the actual code path is
the pre-pass.
Verification:
- `cargo test --workspace`: ALL GREEN. 87 e2e + every crate's unit
+ integration tests pass with no regressions.
- Three NEW in-source tests pin Task 1's resolver paths:
`type_scoped_member_resolves`, `type_scoped_member_not_found`,
`type_scoped_receiver_not_a_type`.
- One NEW workspace test pins the narrowed validator:
`ct1_validator_accepts_bare_with_explicit_import`.
- One renamed-and-flipped existing test:
`ct1_validator_rejects_bare_xmod_with_import_candidate` →
`ct1_validator_accepts_bare_xmod_with_import_candidate` (the
bare-with-import path is now ACCEPTED).
- One NEW companion test for the workspace-wide ctor lookup:
`ct2_term_ctor_bare_cross_module_via_workspace_resolves`.
- Two pre-existing tests' assertions updated for the new error
wording: `ct1_check_cli::check_human_mode_emits_actionable_message_to_stderr`
and `crates/ailang-check/tests/workspace.rs::unknown_module_prefix_is_reported`.
- 12 migrated `.ail` fixtures verified via the existing e2e
suite (each fixture is the test runner's target for an existing
`build_and_run` assertion).
- Negative fixture `ct_2_bare_cross_module.ail` semantically
preserved: dropped its `(import std_maybe)` so bare `Maybe` is
out-of-scope under the narrowed rule and still fires
`BareCrossModuleTypeRef`.
Concerns:
- The pre-pass introduces a new architectural layer (consumer-side
qualification) that the spec did not originally anticipate. Spec
amendment in this commit documents the layer. Future iterations
reference `prepare_workspace_for_check` as established
infrastructure.
- `examples/test_ct1_bare_xmod_rejected.ail.json` switched its
offending name from bare `Ordering` (which under the prep.1
semantics may now resolve via implicit prelude) to a still-
unresolvable `Mystery_Type`. The CLI test's intent (assert that
a human-mode `ail check` exits non-zero on a still-RED case) is
preserved.
Milestone status: kernel-extension-mechanics (Gitea #6) advances
1/3 iters. Next: prep.2 (`Term::New` construct) issue #32.
This commit is contained in:
@@ -318,9 +318,9 @@ pub enum WorkspaceLoadError {
|
||||
/// `candidates` lists the qualified forms found by scanning the
|
||||
/// owning module's imports in declaration order.
|
||||
#[error(
|
||||
"module `{module}` contains bare type name `{name}` that does not resolve to a local type. \
|
||||
AILang's `.ail.json` requires cross-module type references to be qualified. \
|
||||
Candidates from imports: {candidates:?}. Run `ail migrate-canonical-types` to fix legacy fixtures."
|
||||
"module `{module}` references bare type `{name}`, which is not in scope. \
|
||||
Add `(import <module>)` to bring it into scope (candidates: {candidates:?}), \
|
||||
or rewrite the call to type-scoped form `<TypeName>.<member>`."
|
||||
)]
|
||||
BareCrossModuleTypeRef {
|
||||
module: String,
|
||||
@@ -333,8 +333,9 @@ pub enum WorkspaceLoadError {
|
||||
/// workspace, or `<owner>` is known but declares no TypeDef
|
||||
/// named `<type>`.
|
||||
#[error(
|
||||
"module `{module}` references qualified type `{name}` but the owner module is not known \
|
||||
or does not declare a type by that name"
|
||||
"module `{module}` references qualified type `{name}` but the owner module \
|
||||
is not known in the workspace, or it declares no type by that name. \
|
||||
Use the bare type-name from an imported module instead."
|
||||
)]
|
||||
BadCrossModuleTypeRef {
|
||||
module: String,
|
||||
@@ -1109,9 +1110,11 @@ fn check_type_con_name(
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
// Bare cross-module — collect candidates from imports in declaration order.
|
||||
// Prelude is implicit: scan it last as a fallback candidate (mirrors
|
||||
// iter 23.2.4's implicit-prelude behaviour in codegen).
|
||||
// prep.1: a bare cross-module type-name is ACCEPTED if it is in
|
||||
// scope via an explicit import or an implicit (prelude / kernel-
|
||||
// tier) auto-import. Candidates from those paths drive both
|
||||
// acceptance and (on miss) the error message's suggested fix.
|
||||
let mut in_scope = false;
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
for imp in import_names {
|
||||
if local_types
|
||||
@@ -1119,6 +1122,7 @@ fn check_type_con_name(
|
||||
.map(|s| s.contains(name))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
in_scope = true;
|
||||
candidates.push(format!("{imp}.{name}"));
|
||||
}
|
||||
}
|
||||
@@ -1131,9 +1135,13 @@ fn check_type_con_name(
|
||||
.map(|s| s.contains(name))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
in_scope = true;
|
||||
candidates.push(format!("{implicit}.{name}"));
|
||||
}
|
||||
}
|
||||
if in_scope {
|
||||
return Ok(());
|
||||
}
|
||||
Err(WorkspaceLoadError::BareCrossModuleTypeRef {
|
||||
module: owning_module.to_string(),
|
||||
name: name.to_string(),
|
||||
@@ -1855,21 +1863,46 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// a bare Type::Con whose `name` resolves to one imported
|
||||
/// module's TypeDef must fire `BareCrossModuleTypeRef` with that
|
||||
/// qualified form in `candidates` (so the diagnostic can suggest the
|
||||
/// fix).
|
||||
/// prep.1: a bare `Type::Con` whose `name` matches a `TypeDef` in
|
||||
/// an EXPLICITLY imported module must be ACCEPTED. (Pre-prep.1 this
|
||||
/// case was rejected as `BareCrossModuleTypeRef`; type-scoped form
|
||||
/// is canonical and importing the owner module brings the bare
|
||||
/// type-name into scope.)
|
||||
#[test]
|
||||
fn ct1_validator_rejects_bare_xmod_with_import_candidate() {
|
||||
fn ct1_validator_accepts_bare_with_explicit_import() {
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("other".to_string(), module_with_type_def("other", "Ordering"));
|
||||
modules.insert("m".to_string(), module_with_import_and_type_con("m", "other", "Ordering"));
|
||||
validate_canonical_type_names(&modules, &["prelude"])
|
||||
.expect("bare `Ordering` must be accepted when `other` is imported and declares it");
|
||||
}
|
||||
|
||||
/// prep.1: a bare `Type::Con` whose `name` is a `TypeDef` in some
|
||||
/// workspace module BUT the owning module does NOT import that
|
||||
/// module (and the type is not implicit-imported either) must still
|
||||
/// fire `BareCrossModuleTypeRef`. The presence of `other` in the
|
||||
/// workspace is not enough; the bare-in-scope path requires `m` to
|
||||
/// declare an `(import other)`.
|
||||
#[test]
|
||||
fn ct1_validator_rejects_bare_when_owner_not_imported() {
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("other".to_string(), module_with_type_def("other", "Ordering"));
|
||||
// `m` references `Ordering` but does NOT import `other`.
|
||||
modules.insert("m".to_string(), single_module_with_type_con("m", "Ordering").remove("m").unwrap());
|
||||
let err = validate_canonical_type_names(&modules, &["prelude"])
|
||||
.expect_err("Ordering must be rejected with candidate");
|
||||
.expect_err("bare `Ordering` must be rejected when `other` not imported");
|
||||
match err {
|
||||
WorkspaceLoadError::BareCrossModuleTypeRef { name, candidates, .. } => {
|
||||
WorkspaceLoadError::BareCrossModuleTypeRef { name, candidates, module } => {
|
||||
assert_eq!(module, "m");
|
||||
assert_eq!(name, "Ordering");
|
||||
assert_eq!(candidates, vec!["other.Ordering".to_string()]);
|
||||
// No imports on `m` => no candidates list (today's
|
||||
// implementation scans `import_names`, which is empty
|
||||
// here; the workspace-wide scan is not part of the
|
||||
// candidate-list construction).
|
||||
assert!(
|
||||
candidates.is_empty(),
|
||||
"no imports => no candidates; got {candidates:?}"
|
||||
);
|
||||
}
|
||||
other => panic!("expected BareCrossModuleTypeRef, got {other:?}"),
|
||||
}
|
||||
@@ -1941,16 +1974,19 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// a `Term::Ctor` whose `type_name` is a bare cross-module ref
|
||||
/// must fire `BareCrossModuleTypeRef`. Symmetric to the Type::Con
|
||||
/// rule but the field lives on Term, not Type.
|
||||
/// prep.1: a `Term::Ctor` whose `type_name` is a bare type-name
|
||||
/// from a module in scope (here `prelude`, via implicit imports)
|
||||
/// is ACCEPTED. The symmetry with the `Type::Con` rule holds:
|
||||
/// bare-in-scope is canonical post-prep.1, and the implicit
|
||||
/// import path is one of the in-scope paths.
|
||||
#[test]
|
||||
fn ct1_validator_rejects_bare_term_ctor_type_name() {
|
||||
fn ct1_validator_accepts_bare_term_ctor_via_implicit_import() {
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("prelude".to_string(),
|
||||
module_with_type_def("prelude", "Ordering"));
|
||||
// Module `m` has no imports, no local Ordering, but a Term::Ctor
|
||||
// referencing bare `Ordering`.
|
||||
// Module `m` has no explicit imports, no local Ordering, but a
|
||||
// Term::Ctor referencing bare `Ordering` — accepted via the
|
||||
// implicit-prelude path.
|
||||
let m: Module = serde_json::from_value(serde_json::json!({
|
||||
"schema": crate::SCHEMA,
|
||||
"name": "m",
|
||||
@@ -1969,17 +2005,8 @@ mod tests {
|
||||
}],
|
||||
})).unwrap();
|
||||
modules.insert("m".to_string(), m);
|
||||
let err = validate_canonical_type_names(&modules, &["prelude"])
|
||||
.expect_err("bare Term::Ctor type must be rejected");
|
||||
match err {
|
||||
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, candidates } => {
|
||||
assert_eq!(module, "m");
|
||||
assert_eq!(name, "Ordering");
|
||||
assert_eq!(candidates, vec!["prelude.Ordering".to_string()],
|
||||
"prelude is implicit-fallback");
|
||||
}
|
||||
other => panic!("expected BareCrossModuleTypeRef, got {other:?}"),
|
||||
}
|
||||
validate_canonical_type_names(&modules, &["prelude"])
|
||||
.expect("bare Term::Ctor type must be accepted via implicit prelude import");
|
||||
}
|
||||
|
||||
/// a qualified `ClassDef.name` (e.g. `other.Eq`) must fire
|
||||
|
||||
@@ -448,19 +448,25 @@ fn constraint_with_unbound_var_fires_unbound_constraint_type_var() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
/// 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 Ordering");
|
||||
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, "Ordering");
|
||||
assert_eq!(candidates, vec!["prelude.Ordering".to_string()]);
|
||||
assert_eq!(name, "Mystery_Type");
|
||||
assert!(
|
||||
candidates.is_empty(),
|
||||
"no module declares Mystery_Type => candidates empty; got {candidates:?}"
|
||||
);
|
||||
}
|
||||
other => panic!("expected BareCrossModuleTypeRef, got {other:?}"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user