All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
28 KiB
ctt.2 — Registry.type_def_module re-key — Implementation Plan
Parent spec:
docs/specs/0021-ct-tidy.mdFor agentic workers: REQUIRED SUB-SKILL: use
skills/implementto run this plan. Steps use- [ ]checkboxes for tracking.
Goal: Re-key Registry.type_def_module from
BTreeMap<String, String> (bare-name keyed, silently
collision-prone) to BTreeMap<(String, String), String> (keyed by
(owning_module, bare_name)); thread caller_module: &str through
normalize_type_for_registry and its public peer
Registry::normalize_type_for_lookup; update every consumer site;
add a regression fixture pair that exercises the bare-name
collision shape.
Architecture: RED-first. Task 1 lands a two-module fixture pair
where both modules declare type Foo with distinct ctors plus a
class-method instance each; today this trips
DuplicateInstance because both instances canonicalise to
<winner>.Foo after the bare-name overwrite — test asserts
successful load + both instances retrievable in the registry, so
it goes RED today. Task 2 applies the re-key in one atomic edit
pass touching the field, the doc-comment, both pass-1 collection
sites, both pass-2 lookup sites, both signatures plus body of
normalize_type_for_registry / Registry::normalize_type_for_lookup,
the four ailang-check consumer sites at lib.rs and mono.rs, and
the in-workspace.rs test at line 2523. Task 3 confirms the new
test goes GREEN and the full cargo test --workspace stays green.
Tech Stack: ailang-core (Registry, workspace.rs),
ailang-check (lib.rs:1640 instance lookup at NoInstance-check;
mono.rs:121, 624, 1175 instance lookups in monomorphisation).
Files this plan creates or modifies:
- Create:
examples/ctt2_collision_main.ail.json— entry module: importsctt2_collision_lib; declarestype Foo = MkMain; declares classMyC a { op : a -> Int }; declaresinstance MyC main.Foo { op = lam _. 1 }. - Create:
examples/ctt2_collision_lib.ail.json— library module: imports the entry-module's class via qualifier (main.MyC); declarestype Foo = MkLib; declaresinstance main.MyC lib.Foo { op = lam _. 2 }. - Create:
crates/ailang-core/tests/ctt2_registry_rekey.rs— loads the fixture pair viaload_workspace, assertsws.registry.entriescontains two(MyC, type_hash)entries with distinct keys (one formain.Foo, one forlib.Foo), each pointing to its own defining-module. - Modify:
crates/ailang-core/src/workspace.rs:97-106— doc-comment (:97-105) replaced with new invariant; field type at:106changes toBTreeMap<(String, String), String>. - Modify:
crates/ailang-core/src/workspace.rs:121-123—Registry::normalize_type_for_lookupgainscaller_module: &str. - Modify:
crates/ailang-core/src/workspace.rs:537, 546-548— pass-1 collection: declaration changes to tuple-keyed map; insert uses(mod_name.clone(), t.name.clone())as the key. - Modify:
crates/ailang-core/src/workspace.rs:656-659— pass-2 coherence-check lookup uses(mod_name.clone(), type_repr.clone()). - Modify:
crates/ailang-core/src/workspace.rs:678-680— pass-2 canonical-form normalisation passesmod_nameascaller_module. - Modify:
crates/ailang-core/src/workspace.rs:879-928—normalize_type_for_registrysignature gainscaller_module: &str; theType::Conarm's bare-name lookup uses the tuple key; recursive calls threadcaller_modulethrough; doc-comment at:879-883updated. - Modify:
crates/ailang-core/src/workspace.rs:2523-2566— the existingct1_5a_normalize_recurses_into_forall_constraintstest updates its inline map type, insert key, and call to pass a synthetic caller module. - Modify:
crates/ailang-check/src/lib.rs:1639-1640—normalize_type_for_lookup(&r_ty)becomesnormalize_type_for_lookup(env.current_module.as_str(), &r_ty). - Modify:
crates/ailang-check/src/mono.rs:120-121—ws_owned.registry.normalize_type_for_lookup(type_)becomesws_owned.registry.normalize_type_for_lookup(defining_module.as_str(), type_). - Modify:
crates/ailang-check/src/mono.rs:623-624—env.workspace_registry.normalize_type_for_lookup(&r_ty)becomesenv.workspace_registry.normalize_type_for_lookup(module_name, &r_ty). - Modify:
crates/ailang-check/src/mono.rs:1174-1175—env.workspace_registry.normalize_type_for_lookup(&r_ty)becomesenv.workspace_registry.normalize_type_for_lookup(module_name, &r_ty).
Design notes (settled by Boss pre-plan):
- At
workspace.rs:656-659,caller_moduleismod_name(the instance's own module). Rationale: "from this instance's view, where is the type it's on defined?" Under coherence, that is either the instance's module (when type lives there) or the class's module (matched via the existingclass_mod == mod_nameleg). The<primitive-or-unknown>fallback is preserved. - At
mono.rs:121,caller_moduleisdefining_module(the instance's defining module, lifted fromMonoTarget::ClassMethod). Rationale: post-ct.1 thetype_field is already canonical-form (qualified), so the bare-name lookup branch atnormalize_type_for_registry's line 892-equivalent is dead at this site; the threadedcaller_moduleis a structurally-correct no-op. ExtendingMonoTargetwith a new field would be ctt.2-out-of-scope plumbing. - At
mono.rs:624and:1175,caller_moduleismodule_name(the enclosing function parameter tocollect_mono_targets/collect_residuals_ordered). Rationale: that is the module the call was authored in. - At
lib.rs:1640,caller_moduleisenv.current_module.as_str(). Rationale:env.current_modulecarries the active per-module overlay duringcheck_fn, set atlib.rs:1342-1357's caller.
Task 1: RED — fixture pair + regression test
Files:
- Create:
examples/ctt2_collision_main.ail.json - Create:
examples/ctt2_collision_lib.ail.json - Create:
crates/ailang-core/tests/ctt2_registry_rekey.rs
Goal: introduce a workspace that exercises the bare-name
collision the spec fixes. Today both modules' bare-Foo instance
canonicalises to <winner>.Foo via normalize_type_for_registry,
so the second-registered instance trips
workspace.rs:683's DuplicateInstance check. The new test
asserts successful load + both (MyC, type_hash) entries in the
registry → goes RED today.
- Step 1: Write
examples/ctt2_collision_lib.ail.json
Path: examples/ctt2_collision_lib.ail.json
{
"schema": "ailang/v0",
"name": "ctt2_collision_lib",
"imports": [{ "module": "ctt2_collision_main" }],
"defs": [
{
"kind": "type",
"name": "Foo",
"ctors": [{ "name": "MkLib", "fields": [] }]
},
{
"kind": "instance",
"class": "ctt2_collision_main.MyC",
"type": { "k": "con", "name": "Foo" },
"methods": [
{
"name": "op",
"body": {
"t": "lam",
"params": ["_x"],
"paramTypes": [{ "k": "con", "name": "Foo" }],
"retType": { "k": "con", "name": "Int" },
"body": { "t": "lit", "lit": { "kind": "int", "value": 2 } }
}
}
]
}
]
}
- Step 2: Write
examples/ctt2_collision_main.ail.json
Path: examples/ctt2_collision_main.ail.json
{
"schema": "ailang/v0",
"name": "ctt2_collision_main",
"imports": [{ "module": "ctt2_collision_lib" }],
"defs": [
{
"kind": "class",
"name": "MyC",
"param": "a",
"methods": [
{
"name": "op",
"type": {
"k": "fn",
"params": [{ "k": "var", "name": "a" }],
"ret": { "k": "con", "name": "Int" },
"effects": []
}
}
]
},
{
"kind": "type",
"name": "Foo",
"ctors": [{ "name": "MkMain", "fields": [] }]
},
{
"kind": "instance",
"class": "MyC",
"type": { "k": "con", "name": "Foo" },
"methods": [
{
"name": "op",
"body": {
"t": "lam",
"params": ["_x"],
"paramTypes": [{ "k": "con", "name": "Foo" }],
"retType": { "k": "con", "name": "Int" },
"body": { "t": "lit", "lit": { "kind": "int", "value": 1 } }
}
}
]
}
]
}
Note: the entry-module ctt2_collision_main imports
ctt2_collision_lib (lib-side type definition reachable), and
ctt2_collision_lib imports back the main module to see MyC.
Two-way import is supported; workspace loader DFS handles cycles
at module-name granularity.
- Step 3: Write the regression test
Path: crates/ailang-core/tests/ctt2_registry_rekey.rs
//! Regression for the `Registry.type_def_module` re-key (ctt.2).
//!
//! Two modules each declare `type Foo` with distinct ctors and
//! provide an `instance MyC Foo`. Pre-ctt.2, both bare `Foo`s
//! collide on the bare-name `BTreeMap<String, String>` key —
//! `normalize_type_for_registry` qualifies both to whichever
//! module won the insert race, and the loser's instance trips
//! a false `DuplicateInstance`. Post-ctt.2, the tuple-keyed map
//! distinguishes the two `Foo`s by owning module; both instances
//! register cleanly under distinct canonical keys.
//!
//! This test goes red today (DuplicateInstance) and green after
//! the re-key.
use ailang_core::canonical;
use ailang_core::ast::Type;
use ailang_core::load_workspace;
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/)")
.parent().expect("crates has a parent (workspace root)")
.join("examples")
}
#[test]
fn two_modules_with_same_bare_foo_both_register() {
let entry = examples_dir().join("ctt2_collision_main.ail.json");
let ws = load_workspace(&entry)
.expect("expected workspace load to succeed; both `type Foo` declarations live in distinct modules and must register under distinct canonical keys");
// Compute the canonical type hashes for the two expected entries.
let main_foo_hash = canonical::type_hash(&Type::Con {
name: "ctt2_collision_main.Foo".into(),
args: vec![],
});
let lib_foo_hash = canonical::type_hash(&Type::Con {
name: "ctt2_collision_lib.Foo".into(),
args: vec![],
});
let main_key = ("MyC".to_string(), main_foo_hash);
let lib_key = ("MyC".to_string(), lib_foo_hash);
assert!(
ws.registry.entries.contains_key(&main_key),
"registry missing entry for (MyC, ctt2_collision_main.Foo); entries: {:?}",
ws.registry.entries.keys().collect::<Vec<_>>()
);
assert!(
ws.registry.entries.contains_key(&lib_key),
"registry missing entry for (MyC, ctt2_collision_lib.Foo); entries: {:?}",
ws.registry.entries.keys().collect::<Vec<_>>()
);
// Defining-module sanity: each entry's defining_module matches
// the module that wrote the instance.
let main_entry = &ws.registry.entries[&main_key];
assert_eq!(
main_entry.defining_module, "ctt2_collision_main",
"main-side entry's defining_module mismatched"
);
let lib_entry = &ws.registry.entries[&lib_key];
assert_eq!(
lib_entry.defining_module, "ctt2_collision_lib",
"lib-side entry's defining_module mismatched"
);
}
- Step 4: Run the test, confirm RED
Run: cargo test -p ailang-core --test ctt2_registry_rekey two_modules_with_same_bare_foo_both_register
Expected: FAIL. The most likely failure shape today is either:
- (a)
load_workspacereturnsErr(DuplicateInstance { class: "MyC", type_repr: "Foo", first_module: <winner>, second_module: <loser> })because both bare-Fooinstances canonicalise to<winner>.Fooand collide on the(MyC, type_hash)key. The test'sexpect(...)panics with the supplied message. - (b) Less likely but possible: the load succeeds but one of the two
main.Foo/lib.Foohashes is missing fromws.registry.entriesbecause both registered under the same key (last-write-wins). One of the twocontains_keyasserts panics.
If the failure shape is neither (a) nor (b), the spec's diagnosis is wrong and the iter pauses for a fresh read.
Task 2: GREEN — atomic re-key edit pass
Files:
- Modify:
crates/ailang-core/src/workspace.rs(multi-line) - Modify:
crates/ailang-check/src/lib.rs:1639-1640 - Modify:
crates/ailang-check/src/mono.rs:120-121, 623-624, 1174-1175
One edit pass — between Step 1 and the end of Step 8 the workspace will not compile. That is fine; the Boss is committing the iter as a whole, and any intermediate-step partial state is internal to the implement-phase.
- Step 1: Update doc-comment + field type at workspace.rs:97-106
old_string:
/// `N.Foo` to whichever module wins the race. Acceptable for the
/// current corpus (all in-tree fixtures use distinct bare type
/// names across modules); revisit if a future workspace breaks the
/// assumption. Proper fix is to re-key as
/// `(owning_module, bare_name) -> defining_module` and thread the
/// calling module through every consumer site — out of ct.1's scope.
pub type_def_module: BTreeMap<String, String>,
new_string:
/// Keyed by `(owning_module, bare_name)`, value is the
/// defining module. The tuple key disambiguates same-named
/// types declared in different modules — bare `Foo` from
/// module M is `(M, "Foo")`, bare `Foo` from module N is
/// `(N, "Foo")`, and the two carry distinct canonical
/// qualifications under
/// [`normalize_type_for_registry`]. Pre-ctt.2 the key was
/// the bare name alone, and a workspace with two `type Foo`
/// declarations silently overwrote one entry, then tripped
/// `DuplicateInstance` on the loser-side instance after
/// both qualified to `<winner>.Foo`.
pub type_def_module: BTreeMap<(String, String), String>,
- Step 2: Update
Registry::normalize_type_for_lookupsignature/body
old_string:
/// ct.1.5a: produce the canonical form of `t` for registry-key
/// hashing. Bare-non-primitive `Type::Con` names get qualified to
/// `<defining_module>.<name>`; already-qualified names stay; bare
/// names whose defining module is unknown stay as-is.
/// `Type::Fn`/`Type::Forall`/`Type::Var` recurse / pass through
/// structurally.
///
/// Every consumer that hashes an `inst.type_`-shaped expression to
/// look it up in [`Self::entries`] must funnel through this
/// helper, otherwise the registered-form and the queried-form
/// disagree on whether the leading qualifier is present.
pub fn normalize_type_for_lookup(&self, t: &Type) -> Type {
normalize_type_for_registry(t, &self.type_def_module)
}
new_string:
/// ct.1.5a + ctt.2: produce the canonical form of `t` for
/// registry-key hashing. Bare-non-primitive `Type::Con` names
/// get qualified to `<defining_module>.<name>`; already-qualified
/// names stay; bare names whose defining module is unknown stay
/// as-is. `Type::Fn`/`Type::Forall`/`Type::Var` recurse / pass
/// through structurally.
///
/// `caller_module` is the module in whose scope `t` was authored.
/// Bare-name lookups are keyed by `(caller_module, name)`, so a
/// bare `Foo` written in module M resolves only to M's `Foo`,
/// never to a same-named type from another module.
///
/// Every consumer that hashes an `inst.type_`-shaped expression
/// to look it up in [`Self::entries`] must funnel through this
/// helper, otherwise the registered-form and the queried-form
/// disagree on whether the leading qualifier is present.
pub fn normalize_type_for_lookup(&self, caller_module: &str, t: &Type) -> Type {
normalize_type_for_registry(caller_module, t, &self.type_def_module)
}
- Step 3: Update pass-1 collection declaration + insert
Apply Edit 1 to crates/ailang-core/src/workspace.rs:
old_string:
let mut type_def_module: BTreeMap<String, String> = BTreeMap::new();
new_string:
let mut type_def_module: BTreeMap<(String, String), String> = BTreeMap::new();
Apply Edit 2 to crates/ailang-core/src/workspace.rs:
old_string:
Def::Type(t) => {
type_def_module.insert(t.name.clone(), mod_name.clone());
}
new_string:
Def::Type(t) => {
type_def_module.insert(
(mod_name.clone(), t.name.clone()),
mod_name.clone(),
);
}
- Step 4: Update pass-2 coherence-check lookup + canonical-form normalisation
Apply Edit 1 to crates/ailang-core/src/workspace.rs (pass-2
type_mod lookup):
old_string:
let type_mod = type_def_module
.get(&type_repr)
.cloned()
.unwrap_or_else(|| "<primitive-or-unknown>".into());
new_string:
let type_mod = type_def_module
.get(&(mod_name.clone(), type_repr.clone()))
.cloned()
.unwrap_or_else(|| "<primitive-or-unknown>".into());
Apply Edit 2 to crates/ailang-core/src/workspace.rs (pass-2
canonical-form hashing):
old_string:
let type_hash = canonical::type_hash(
&normalize_type_for_registry(&inst.type_, &type_def_module),
);
new_string:
let type_hash = canonical::type_hash(
&normalize_type_for_registry(
mod_name,
&inst.type_,
&type_def_module,
),
);
- Step 5: Update
normalize_type_for_registrysignature, doc-comment, body
old_string:
/// `type_def_module`), not the instance's owning module. The two
/// coincide under a canonical-form-compliant workspace (bare implies
/// local-to-defining-module), but using the defining-module lookup is
/// robust against pre-`validate_canonical_type_names`-wired fixtures
/// that may still carry bare cross-module refs.
fn normalize_type_for_registry(
t: &Type,
type_def_module: &BTreeMap<String, String>,
) -> Type {
match t {
Type::Con { name, args } => {
let new_name = if name.contains('.') || is_primitive_type_name(name) {
name.clone()
} else if let Some(owner) = type_def_module.get(name) {
format!("{owner}.{name}")
} else {
// Unknown bare non-primitive — leave as-is. Either it is
// a class-param Type::Var miscoded as a Con (which is a
// separate well-formedness problem) or a stale ref that
// downstream diagnostics will catch.
name.clone()
};
Type::Con {
name: new_name,
args: args
.iter()
.map(|a| normalize_type_for_registry(a, type_def_module))
.collect(),
}
}
Type::Fn { params, param_modes, ret, ret_mode, effects } => Type::Fn {
params: params
.iter()
.map(|p| normalize_type_for_registry(p, type_def_module))
.collect(),
param_modes: param_modes.clone(),
ret: Box::new(normalize_type_for_registry(ret, type_def_module)),
ret_mode: *ret_mode,
effects: effects.clone(),
},
Type::Forall { vars, constraints, body } => Type::Forall {
vars: vars.clone(),
constraints: constraints
.iter()
.map(|c| crate::ast::Constraint {
class: c.class.clone(),
type_: normalize_type_for_registry(&c.type_, type_def_module),
})
.collect(),
body: Box::new(normalize_type_for_registry(body, type_def_module)),
},
Type::Var { name } => Type::Var { name: name.clone() },
}
}
new_string:
/// `type_def_module`), not the instance's owning module. The two
/// coincide under a canonical-form-compliant workspace (bare implies
/// local-to-caller-module), but using the defining-module lookup is
/// robust against pre-`validate_canonical_type_names`-wired fixtures
/// that may still carry bare cross-module refs.
///
/// ctt.2: bare-name lookups are keyed by `(caller_module, name)`,
/// not by `name` alone. A bare `Foo` written from module M resolves
/// to M's `Foo` only; same-named types in other modules are
/// distinct entries under their own caller-keyed tuple.
fn normalize_type_for_registry(
caller_module: &str,
t: &Type,
type_def_module: &BTreeMap<(String, String), String>,
) -> Type {
match t {
Type::Con { name, args } => {
let new_name = if name.contains('.') || is_primitive_type_name(name) {
name.clone()
} else if let Some(owner) =
type_def_module.get(&(caller_module.to_string(), name.clone()))
{
format!("{owner}.{name}")
} else {
// Unknown bare non-primitive — leave as-is. Either it is
// a class-param Type::Var miscoded as a Con (which is a
// separate well-formedness problem) or a stale ref that
// downstream diagnostics will catch.
name.clone()
};
Type::Con {
name: new_name,
args: args
.iter()
.map(|a| normalize_type_for_registry(caller_module, a, type_def_module))
.collect(),
}
}
Type::Fn { params, param_modes, ret, ret_mode, effects } => Type::Fn {
params: params
.iter()
.map(|p| normalize_type_for_registry(caller_module, p, type_def_module))
.collect(),
param_modes: param_modes.clone(),
ret: Box::new(normalize_type_for_registry(caller_module, ret, type_def_module)),
ret_mode: *ret_mode,
effects: effects.clone(),
},
Type::Forall { vars, constraints, body } => Type::Forall {
vars: vars.clone(),
constraints: constraints
.iter()
.map(|c| crate::ast::Constraint {
class: c.class.clone(),
type_: normalize_type_for_registry(caller_module, &c.type_, type_def_module),
})
.collect(),
body: Box::new(normalize_type_for_registry(caller_module, body, type_def_module)),
},
Type::Var { name } => Type::Var { name: name.clone() },
}
}
- Step 6: Update the inline test at workspace.rs:2523-2566
Apply Edit 1 to crates/ailang-core/src/workspace.rs (map type +
insert key):
old_string:
let mut type_def_module: BTreeMap<String, String> = BTreeMap::new();
type_def_module.insert("MyInt".to_string(), "other".to_string());
new_string:
let mut type_def_module: BTreeMap<(String, String), String> = BTreeMap::new();
// Caller module is "caller" for this test; the type `MyInt`
// lives in `other`. Under the tuple key the bare-name lookup
// resolves only when the caller is "caller".
type_def_module.insert(
("caller".to_string(), "MyInt".to_string()),
"other".to_string(),
);
Apply Edit 2 to crates/ailang-core/src/workspace.rs (call site):
old_string:
let out = normalize_type_for_registry(&input, &type_def_module);
new_string:
let out = normalize_type_for_registry("caller", &input, &type_def_module);
- Step 7: Update ailang-check consumer sites
Apply Edit 1 to crates/ailang-check/src/lib.rs:1639-1640 (the
exact surrounding context to disambiguate; the call site is the
NoInstance check inside check_fn where env: &Env is in scope):
old_string:
.normalize_type_for_lookup(&r_ty)
new_string:
.normalize_type_for_lookup(env.current_module.as_str(), &r_ty)
Note: if the literal .normalize_type_for_lookup(&r_ty) appears at
multiple sites in lib.rs, the implementer disambiguates using a
larger surrounding-context Edit (the spec-recon report identified
this as a single site at line 1639-1640 — adjust if recon's
line number is stale).
Apply Edit 2 to crates/ailang-check/src/mono.rs:120-121 (the
MonoTarget::ClassMethod site inside monomorphise_workspace;
defining_module is in scope from the destructured pattern):
old_string:
ws_owned.registry.normalize_type_for_lookup(type_)
new_string:
ws_owned.registry.normalize_type_for_lookup(defining_module.as_str(), type_)
Apply Edit 3 to crates/ailang-check/src/mono.rs:623-624 (the
collect_mono_targets site; module_name: &str is in scope from
the enclosing fn signature):
old_string:
env.workspace_registry.normalize_type_for_lookup(&r_ty)
new_string:
env.workspace_registry.normalize_type_for_lookup(module_name, &r_ty)
Note: if env.workspace_registry.normalize_type_for_lookup(&r_ty)
matches both the 623-624 site and the 1174-1175 site, use a larger
surrounding-context Edit for each. The two enclosing functions
(collect_mono_targets and collect_residuals_ordered) both have
module_name: &str in scope; the new call form is identical.
Apply Edit 4 to crates/ailang-check/src/mono.rs:1174-1175 (the
collect_residuals_ordered site; same shape as Edit 3 — if Edit 3
matched both already via replace_all, this step is a no-op
verification rather than a separate edit).
- Step 8: Build, confirm no compile errors
Run: cargo build --workspace
Expected: clean build, no errors. If errors surface, they will
most likely be:
- One of the four
ailang-checkcall sites was missed and still passes the old single-argument form. - A test under
crates/other than the ones named in the Files-listing referencesnormalize_type_for_lookupornormalize_type_for_registryand needs the same threading. - An
ailang-codegenconsumer surfaces (recon did not see one, but a compile error is the catch-all).
For any such surface, apply the same caller_module threading
pattern and re-run cargo build --workspace.
Task 3: Workspace-wide test gate
Files: (verification only — no edits)
- Step 1: Run the ctt.2 regression test, confirm GREEN
Run: cargo test -p ailang-core --test ctt2_registry_rekey two_modules_with_same_bare_foo_both_register
Expected: PASS. Both (MyC, type_hash) entries land in
ws.registry.entries; the test's two assert! calls succeed.
- Step 2: Run the entire workspace test suite
Run: cargo test --workspace
Expected: all tests pass, including:
- The updated
ct1_5a_normalize_recurses_into_forall_constraints(workspace.rs:2523). - The new
two_modules_with_same_bare_foo_both_register. - Every existing test in
ailang-core,ailang-check,ailang-codegen,ailang-prose,ailang-surface,ail.
If any existing test fails: the most plausible cause is a fixture
under examples/ that legitimately exercises a bare cross-module
type reference and depended on the silent-overwrite behaviour.
Under the tuple key such a reference now misses the lookup and
the corresponding instance falls into the <primitive-or-unknown>
coherence path. Inspect the failing fixture; if the failure is the
canonical-form-violation it is structurally diagnosing, the fixture
is the bug and the test is the win — fix the fixture by qualifying
the cross-module type reference.
- Step 3: Bench scripts are deferred to audit
The audit skill at milestone close runs bench/check.py,
bench/compile_check.py, bench/cross_lang.py. ctt.2 makes
zero codegen-relevant edits (the registry is workspace-load-time
metadata, not runtime), so no bench movement is plausibly
attributable to it; the audit confirms.
Acceptance criteria recap (from spec)
Registry.type_def_modulekey is(String, String), value isString. ✓ Task 2 Step 1.- Every consumer site provides the calling module at the call site. ✓ Task 2 Steps 2-7.
- A regression test exercises two-module bare-name collision and confirms both instances resolve correctly. ✓ Task 1 + Task 3 Step 1.
cargo test --workspacegreen. ✓ Task 3 Step 2.- Doc-comment at workspace.rs:97-105 carries the new shape's invariant. ✓ Task 2 Step 1.