832375f2ac
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.
760 lines
28 KiB
Markdown
760 lines
28 KiB
Markdown
# ctt.2 — `Registry.type_def_module` re-key — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/0021-ct-tidy.md`
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
|
> to 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:
|
|
imports `ctt2_collision_lib`; declares `type Foo = MkMain`;
|
|
declares class `MyC a { op : a -> Int }`; declares
|
|
`instance 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`); declares `type Foo = MkLib`;
|
|
declares `instance main.MyC lib.Foo { op = lam _. 2 }`.
|
|
- Create: `crates/ailang-core/tests/ctt2_registry_rekey.rs` —
|
|
loads the fixture pair via `load_workspace`, asserts
|
|
`ws.registry.entries` contains two `(MyC, type_hash)` entries
|
|
with distinct keys (one for `main.Foo`, one for `lib.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 `:106`
|
|
changes to `BTreeMap<(String, String), String>`.
|
|
- Modify: `crates/ailang-core/src/workspace.rs:121-123` —
|
|
`Registry::normalize_type_for_lookup` gains `caller_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 passes `mod_name` as `caller_module`.
|
|
- Modify: `crates/ailang-core/src/workspace.rs:879-928` —
|
|
`normalize_type_for_registry` signature gains `caller_module: &str`;
|
|
the `Type::Con` arm's bare-name lookup uses the tuple key;
|
|
recursive calls thread `caller_module` through; doc-comment at
|
|
`:879-883` updated.
|
|
- Modify: `crates/ailang-core/src/workspace.rs:2523-2566` — the
|
|
existing `ct1_5a_normalize_recurses_into_forall_constraints` test
|
|
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)` becomes
|
|
`normalize_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_)` becomes
|
|
`ws_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)` becomes
|
|
`env.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)` becomes
|
|
`env.workspace_registry.normalize_type_for_lookup(module_name, &r_ty)`.
|
|
|
|
**Design notes (settled by Boss pre-plan):**
|
|
|
|
- At `workspace.rs:656-659`, `caller_module` is `mod_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 existing `class_mod ==
|
|
mod_name` leg). The `<primitive-or-unknown>` fallback is
|
|
preserved.
|
|
- At `mono.rs:121`, `caller_module` is `defining_module` (the
|
|
instance's defining module, lifted from `MonoTarget::ClassMethod`).
|
|
Rationale: post-ct.1 the `type_` field is already canonical-form
|
|
(qualified), so the bare-name lookup branch at
|
|
`normalize_type_for_registry`'s line 892-equivalent is dead at
|
|
this site; the threaded `caller_module` is a structurally-correct
|
|
no-op. Extending `MonoTarget` with a new field would be
|
|
ctt.2-out-of-scope plumbing.
|
|
- At `mono.rs:624` and `:1175`, `caller_module` is `module_name`
|
|
(the enclosing function parameter to `collect_mono_targets` /
|
|
`collect_residuals_ordered`). Rationale: that is the module the
|
|
call was authored in.
|
|
- At `lib.rs:1640`, `caller_module` is `env.current_module.as_str()`.
|
|
Rationale: `env.current_module` carries the active per-module
|
|
overlay during `check_fn`, set at `lib.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`
|
|
|
|
```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`
|
|
|
|
```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`
|
|
|
|
```rust
|
|
//! 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_workspace` returns `Err(DuplicateInstance { class: "MyC", type_repr: "Foo", first_module: <winner>, second_module: <loser> })` because both bare-`Foo` instances canonicalise to `<winner>.Foo` and collide on the `(MyC, type_hash)` key. The test's `expect(...)` panics with the supplied message.
|
|
- (b) Less likely but possible: the load succeeds but one of the two `main.Foo` / `lib.Foo` hashes is missing from `ws.registry.entries` because both registered under the same key (last-write-wins). One of the two `contains_key` asserts 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_lookup` signature/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_registry` signature, 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-check` call sites was missed and still
|
|
passes the old single-argument form.
|
|
- A test under `crates/` other than the ones named in the
|
|
Files-listing references `normalize_type_for_lookup` or
|
|
`normalize_type_for_registry` and needs the same threading.
|
|
- An `ailang-codegen` consumer 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_module` key is `(String, String)`, value is
|
|
`String`. ✓ 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 --workspace` green. ✓ Task 3 Step 2.
|
|
- Doc-comment at workspace.rs:97-105 carries the new shape's
|
|
invariant. ✓ Task 2 Step 1.
|