Files
AILang/crates/ailang-check/tests/env_construction_pin.rs
T
Brummel a163c8c34a GREEN: pre-pass/resolver asymmetry — same-module bare + kernel-tier qualifier acceptance (closes F1, F3)
Two minimal edits in ailang-check that bring the resolver's
acceptance set into line with what prep.1's workspace pre-pass
(`prepare_workspace_for_check`) actually produces. The pre-pass
itself is unchanged — its qualification rules are correct per
spec § "Realisation mechanism — workspace pre-pass". RED tests
landed in 4d39fdc; both turn GREEN, both fieldtest fixtures
that surfaced the bugs now `ail check` clean.

F1 — same-module type-scoped call (kem_2b_min_repro.ail).
The dot-qualified `Term::Var` arm in `synth_term`
(`crates/ailang-check/src/lib.rs` ~line 3425) was
unconditionally rewriting the resolved fn signature via
`qualify_local_types`, so a call like `(app Counter.value c)`
from inside Counter's home module produced a return type
`<this>.Counter` — but the matching `Term::Ctor` arm
(~line 3644) and the pre-pass itself both leave own-module
type-names bare. The two halves of the resolver disagreed.

Fix: branch on `target_module == env.current_module` — when
true, skip the qualification (the raw type is already in the
form the rest of the resolver speaks); when false, qualify
as before. `owner_types` lookup moved inside the else-branch
since it's only consulted when qualification runs. A short
comment names the pre-pass and the sibling Term::Ctor arm so
a future reader sees the three-way symmetry.

F3 — kernel-tier qualifier acceptance (kem_3_stub_consumer.ail).
The env-builder paths `build_check_env` (~line 1653) and
`check_in_workspace` (~line 1807) force-injected only
`"prelude"` into `env.imports` / `env.module_imports`. The
loader meanwhile derives the implicit-imports list from
`modules.values().filter(|m| m.kernel)` (since prep.3) — so
`kernel_stub` flows into runtime resolution but not into
the checker's qualifier-validity check (~line 1968). Bare
`StubT` got qualified by the pre-pass to `kernel_stub.StubT`,
then bounced off the validity check as "unknown module
prefix".

Fix at both sites: keep the unconditional `"prelude"` literal
injection AND additively insert every workspace module with
`kernel: true` into the per-module import map (skip
self-injection). Idempotent for production (where prelude
also carries `kernel: true`) and preserves the
`bare_name_resolves_through_implicit_import_to_free_fn`
unit-test contract — that test deliberately constructs a
prelude module with `kernel: false` to pin the legacy
"prelude is always implicit" guarantee per se, independent
of the kernel flag. Mirrors the loader's kernel-flag filter,
making the env-builder and the loader speak the same set.

Sibling pin `crates/ailang-check/tests/env_construction_pin.rs`
(the frozen-reproduction copy of `build_check_env`'s
`module_imports` loop) updated in lockstep so it remains a
meaningful drift detector after the env-builder change. The
pin is intentionally designed to track legitimate behavioural
changes.

Verification:
  - tests::type_scoped_call_in_same_module_resolves      GREEN
  - tests::kernel_tier_module_qualifier_resolves_…       GREEN
  - examples/fieldtest/kem_2b_min_repro.ail              ail check ok
  - examples/fieldtest/kem_3_stub_consumer.ail           ail check ok
  - cargo test --workspace                               666 / 0
  (664 prior baseline + 2 RED→GREEN this iter)

Implementer notes: one re-loop on the F3 site after the first
edit replaced the legacy "prelude" literal injection wholesale
with the kernel-flag filter — that broke the unit-test contract
above. Restored the literal AND added the filter on top per the
"don't adapt tests to bugs" memory. No spec-review or
quality-review loops.

prep.1's pre-pass design (spec § "Realisation mechanism") was
correct as written — the symmetry between owner-side
`qualify_local_types` and consumer-side `prepare_workspace_for_check`
holds. What was missing was uniform resolver-side acceptance of
the qualifiers the pre-pass produces. With these two edits, the
following equivalences hold throughout the checker:

  bare T  ≡  <this-module>.T            (F1, same-module rule)
  bare T  ≡  <kernel-module>.T          (F3, kernel-tier rule)

via the loader-driven implicit-imports list that now drives
both the checker's import map and the type-validity check.
2026-05-28 19:36:36 +02:00

258 lines
9.8 KiB
Rust

//! Drift-shape pin for env construction (env-construction unify
//! milestone). Asserts that `build_check_env(&ws)` agrees with an
//! in-test reproduction of the pre-refactor inline seeding on every
//! workspace-flat field. Future-you adds a new workspace-flat env
//! field without updating `build_check_env` -> this test fails before
//! the bug ships.
//!
//! Fixture: a two-module workspace covering one `Def::Class`, one
//! `Def::Instance`, one user ADT, and one cross-module import (the
//! same shape that `test_mono_imports_*` use).
use ailang_check::{build_check_env, build_module_globals, Env};
use ailang_core::ast::Def;
use ailang_surface::load_workspace;
use std::collections::BTreeMap;
use std::path::Path;
fn examples_dir() -> std::path::PathBuf {
let manifest = env!("CARGO_MANIFEST_DIR");
Path::new(manifest)
.parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)")
.parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)")
.join("examples")
}
/// Independent reproduction of every workspace-flat env seeding
/// step. Pre-refactor, workspace-flat seeding was split between
/// `check_in_workspace` (per-module `types`/`ctor_index`) and
/// `mono::build_workspace_env` (workspace-flat `types`/`ctor_index`
/// plus everything else); this reproduction captures the *union*
/// — exactly the surface `build_check_env` is responsible for.
/// Kept as a frozen reference: any future divergence between this
/// body and `build_check_env` makes the assertion below fail.
fn build_env_inline_pre_refactor(ws: &ailang_core::workspace::Workspace) -> Env {
let mut env = Env::default();
ailang_check::builtins::install(&mut env);
// env.types + env.ctor_index — workspace-flat from every Def::Type.
for m in ws.modules.values() {
for d in &m.defs {
if let Def::Type(td) = d {
for c in &td.ctors {
env.ctor_index.insert(
c.name.clone(),
ailang_check::CtorRef { type_name: td.name.clone() },
);
}
env.types.insert(td.name.clone(), td.clone());
}
}
}
// env.module_globals — per-module .fns projection.
let mg = build_module_globals(ws).expect("build_module_globals");
env.module_globals = mg
.iter()
.map(|(k, v)| (k.clone(), v.fns.clone()))
.collect();
// env.module_types — workspace-wide ADT index, inlined here as
// a frozen reproduction (test does not share helper code with
// production).
let mut module_types: BTreeMap<String, indexmap::IndexMap<String, ailang_core::ast::TypeDef>> = BTreeMap::new();
for (mname, m) in &ws.modules {
let mut tys: indexmap::IndexMap<String, ailang_core::ast::TypeDef> = indexmap::IndexMap::new();
for d in &m.defs {
if let Def::Type(td) = d {
tys.insert(td.name.clone(), td.clone());
}
}
module_types.insert(mname.clone(), tys);
}
env.module_types = module_types;
// env.module_imports — per-module alias map. Iter 23.1 added
// the implicit `prelude` import. bugfix-prepass-resolver-
// asymmetry (F3) generalised it: every workspace module flagged
// `kernel: true` is implicitly imported into every non-self
// module (mirrors the loader filter at
// `crates/ailang-surface/src/loader.rs`). The literal
// `"prelude"` injection stays for back-compat with unit-test
// workspaces that construct a `kernel: false` prelude.
let kernel_modules: Vec<String> = ws
.modules
.values()
.filter(|m| m.kernel)
.map(|m| m.name.clone())
.collect();
for m in ws.modules.values() {
let mut import_map: BTreeMap<String, String> = BTreeMap::new();
for imp in &m.imports {
let key = imp.alias.clone().unwrap_or_else(|| imp.module.clone());
import_map.insert(key, imp.module.clone());
}
if m.name != "prelude" {
import_map
.entry("prelude".to_string())
.or_insert_with(|| "prelude".to_string());
}
for k in &kernel_modules {
if &m.name != k {
import_map.entry(k.clone()).or_insert_with(|| k.clone());
}
}
env.module_imports.insert(m.name.clone(), import_map);
}
// env.class_methods — workspace-merged from each module's
// ModuleGlobals.class_methods.
for g in mg.values() {
for (n, e) in &g.class_methods {
env.class_methods.insert(n.clone(), e.clone());
}
}
// env.class_superclasses — walk every module's Def::Class.
//
// both key and value carry the qualified workspace-key shape;
// bare `SuperclassRef.class` is lifted using the parent class's
// defining module.
for (mod_name, m) in &ws.modules {
for d in &m.defs {
if let Def::Class(cd) = d {
if let Some(sc) = &cd.superclass {
let class_qualified = format!("{mod_name}.{}", cd.name);
let sc_qualified = if sc.class.contains('.') {
sc.class.clone()
} else {
format!("{mod_name}.{}", sc.class)
};
env.class_superclasses.insert(class_qualified, sc_qualified);
}
}
}
}
// env.workspace_registry — clone of ws.registry.
env.workspace_registry = ws.registry.clone();
env
}
#[test]
fn build_check_env_matches_inline_seeding() {
let entry = examples_dir().join("test_mono_imports_main.ail");
let ws = load_workspace(&entry).expect("load test_mono_imports_main");
let env_helper = build_check_env(&ws);
let env_inline = build_env_inline_pre_refactor(&ws);
// env.types — IndexMap<String, TypeDef>: compare key-set + name.
assert_eq!(
env_helper.types.keys().collect::<Vec<_>>(),
env_inline.types.keys().collect::<Vec<_>>(),
"env.types key-set differs"
);
for k in env_helper.types.keys() {
assert_eq!(
env_helper.types[k].name,
env_inline.types[k].name,
"env.types[{k}] type-def name differs"
);
}
// env.ctor_index — IndexMap<String, CtorRef>: compare key-set +
// type_name.
assert_eq!(
env_helper.ctor_index.keys().collect::<Vec<_>>(),
env_inline.ctor_index.keys().collect::<Vec<_>>(),
"env.ctor_index key-set differs"
);
for k in env_helper.ctor_index.keys() {
assert_eq!(
env_helper.ctor_index[k].type_name,
env_inline.ctor_index[k].type_name,
"env.ctor_index[{k}].type_name differs"
);
}
// env.module_globals — BTreeMap<String, IndexMap<String, Type>>.
assert_eq!(
env_helper.module_globals.keys().collect::<Vec<_>>(),
env_inline.module_globals.keys().collect::<Vec<_>>(),
"env.module_globals key-set differs"
);
for k in env_helper.module_globals.keys() {
assert_eq!(
env_helper.module_globals[k].keys().collect::<Vec<_>>(),
env_inline.module_globals[k].keys().collect::<Vec<_>>(),
"env.module_globals[{k}] inner key-set differs"
);
}
// env.module_types — same shape as module_globals.
assert_eq!(
env_helper.module_types.keys().collect::<Vec<_>>(),
env_inline.module_types.keys().collect::<Vec<_>>(),
"env.module_types key-set differs"
);
for k in env_helper.module_types.keys() {
assert_eq!(
env_helper.module_types[k].keys().collect::<Vec<_>>(),
env_inline.module_types[k].keys().collect::<Vec<_>>(),
"env.module_types[{k}] inner key-set differs"
);
}
// env.module_imports — BTreeMap<String, BTreeMap<String, String>>:
// direct equality.
assert_eq!(
env_helper.module_imports, env_inline.module_imports,
"env.module_imports differs"
);
// env.class_methods — BTreeMap<String, ClassMethodEntry>:
// compare key-set (entry shape has no PartialEq).
assert_eq!(
env_helper.class_methods.keys().collect::<Vec<_>>(),
env_inline.class_methods.keys().collect::<Vec<_>>(),
"env.class_methods key-set differs"
);
// env.class_superclasses — direct equality.
assert_eq!(
env_helper.class_superclasses, env_inline.class_superclasses,
"env.class_superclasses differs"
);
// env.effect_ops — IndexMap<String, EffectOpSig>: compare key-set.
assert_eq!(
env_helper.effect_ops.keys().collect::<Vec<_>>(),
env_inline.effect_ops.keys().collect::<Vec<_>>(),
"env.effect_ops key-set differs"
);
// env.workspace_registry — entries key-set must match.
assert_eq!(
env_helper.workspace_registry.entries.keys().collect::<Vec<_>>(),
env_inline.workspace_registry.entries.keys().collect::<Vec<_>>(),
"env.workspace_registry.entries key-set differs"
);
// Per-call overlay fields must NOT be seeded by the helper.
assert_eq!(env_helper.current_module, "", "current_module is overlay");
// env.globals — partially workspace-flat (builtins operators
// installed by `builtins::install`) and partially overlay
// (per-module fns seeded by callers). The helper carries only
// the workspace-flat half; key-set must match the inline
// reproduction at this point.
assert_eq!(
env_helper.globals.keys().collect::<Vec<_>>(),
env_inline.globals.keys().collect::<Vec<_>>(),
"env.globals key-set differs (workspace-flat half from builtins)"
);
assert!(env_helper.imports.is_empty(), "imports is overlay");
assert!(env_helper.rigid_vars.is_empty(), "rigid_vars is overlay");
}