From 9d01d0884cdca7359b0ae2773c639f5574077ecb Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 12 May 2026 22:23:46 +0200 Subject: [PATCH] iter ctt.2: Registry.type_def_module re-key to (owning_module, bare_name) --- .../2026-05-12-iter-ctt.2.json | 13 ++ crates/ailang-check/src/lib.rs | 4 +- crates/ailang-check/src/mono.rs | 12 +- crates/ailang-core/src/workspace.rs | 99 +++++++----- .../ailang-core/tests/ctt2_registry_rekey.rs | 70 +++++++++ docs/journals/2026-05-12-iter-ctt.2.md | 142 ++++++++++++++++++ docs/journals/INDEX.md | 1 + examples/ctt2_collision_cls.ail.json | 23 +++ examples/ctt2_collision_lib.ail.json | 29 ++++ examples/ctt2_collision_main.ail.json | 32 ++++ 10 files changed, 385 insertions(+), 40 deletions(-) create mode 100644 bench/orchestrator-stats/2026-05-12-iter-ctt.2.json create mode 100644 crates/ailang-core/tests/ctt2_registry_rekey.rs create mode 100644 docs/journals/2026-05-12-iter-ctt.2.md create mode 100644 examples/ctt2_collision_cls.ail.json create mode 100644 examples/ctt2_collision_lib.ail.json create mode 100644 examples/ctt2_collision_main.ail.json diff --git a/bench/orchestrator-stats/2026-05-12-iter-ctt.2.json b/bench/orchestrator-stats/2026-05-12-iter-ctt.2.json new file mode 100644 index 0000000..c01a9a9 --- /dev/null +++ b/bench/orchestrator-stats/2026-05-12-iter-ctt.2.json @@ -0,0 +1,13 @@ +{ + "iter_id": "ctt.2", + "date": "2026-05-12", + "mode": "standard", + "outcome": "DONE", + "tasks_total": 3, + "tasks_completed": 3, + "reloops_per_task": { "1": 1, "2": 0, "3": 0 }, + "review_loops_spec": 0, + "review_loops_quality": 0, + "blocked_reason": null, + "notes": "Task 1 had one in-implementer-phase NEEDS_CONTEXT retry: plan's verbatim two-module fixture pair was structurally invalid (Cycle on two-way import + QualifiedClassName on qualified InstanceDef.class). Retry-1 introduced a third class-bearing module to break the cycle and dropped the qualifier from instance heads; the plan's RED-condition (bare-name collision) is preserved. RED shape was OrphanInstance (a related consumer site of the same bare-name keying defect, fixed by the same re-key) rather than the plan's predicted DuplicateInstance. No spec/quality re-loops in any task." +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 0868fed..6dfbb68 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -1637,7 +1637,9 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> { // same key shape `workspace::build_registry` writes with, so // representation-equal types match deterministically. // normalize to match Registry::normalize_type_for_lookup contract - let r_ty_norm = env.workspace_registry.normalize_type_for_lookup(&r_ty); + let r_ty_norm = env + .workspace_registry + .normalize_type_for_lookup(env.current_module.as_str(), &r_ty); let key = ( r.class.clone(), ailang_core::canonical::type_hash(&r_ty_norm), diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index 58b8b06..23e012d 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -118,7 +118,9 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result { let (f, defining_module) = match t { MonoTarget::ClassMethod { class, type_, defining_module, .. } => { // normalize to match Registry::normalize_type_for_lookup contract - let t_ty_norm = ws_owned.registry.normalize_type_for_lookup(type_); + let t_ty_norm = ws_owned + .registry + .normalize_type_for_lookup(defining_module.as_str(), type_); let registry_key = (class.clone(), ailang_core::canonical::type_hash(&t_ty_norm)); let entry = ws_owned @@ -621,7 +623,9 @@ pub fn collect_mono_targets( continue; } // normalize to match Registry::normalize_type_for_lookup contract - let r_ty_norm = env.workspace_registry.normalize_type_for_lookup(&r_ty); + let r_ty_norm = env + .workspace_registry + .normalize_type_for_lookup(module_name, &r_ty); let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty_norm)); let entry = match env.workspace_registry.entries.get(&key) { Some(e) => e, @@ -1172,7 +1176,9 @@ pub(crate) fn collect_residuals_ordered( if !crate::is_fully_concrete(&r_ty) { return None; } - let r_ty_norm = env.workspace_registry.normalize_type_for_lookup(&r_ty); + let r_ty_norm = env + .workspace_registry + .normalize_type_for_lookup(module_name, &r_ty); let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty_norm)); let entry = env.workspace_registry.entries.get(&key)?; Some(MonoTarget::ClassMethod { diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index 945ed51..ed86651 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -93,33 +93,39 @@ pub struct Registry { /// Populated in [`build_registry`] from the same scan that builds /// the entry map. /// - /// Note: keyed by bare type name only. If two modules each define a - /// type with the same bare name (e.g. `type Foo` in both `M` and - /// `N`), the second `insert` overwrites the first, and - /// [`Self::normalize_type_for_lookup`] would collapse `M.Foo` and - /// `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, + /// 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 `.Foo`. + pub type_def_module: BTreeMap<(String, String), String>, } impl Registry { - /// ct.1.5a: produce the canonical form of `t` for registry-key - /// hashing. Bare-non-primitive `Type::Con` names get qualified to - /// `.`; already-qualified names stay; bare - /// names whose defining module is unknown stay as-is. - /// `Type::Fn`/`Type::Forall`/`Type::Var` recurse / pass through - /// structurally. + /// ct.1.5a + ctt.2: produce the canonical form of `t` for + /// registry-key hashing. Bare-non-primitive `Type::Con` names + /// get qualified to `.`; 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 + /// `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, t: &Type) -> Type { - normalize_type_for_registry(t, &self.type_def_module) + pub fn normalize_type_for_lookup(&self, caller_module: &str, t: &Type) -> Type { + normalize_type_for_registry(caller_module, t, &self.type_def_module) } } @@ -534,7 +540,7 @@ fn build_registry( // Pass 1: collect "where is X defined" maps, plus a class lookup // by name (needed for the method-completeness check). let mut class_def_module: BTreeMap = BTreeMap::new(); - let mut type_def_module: BTreeMap = BTreeMap::new(); + let mut type_def_module: BTreeMap<(String, String), String> = BTreeMap::new(); let mut class_by_name: BTreeMap = BTreeMap::new(); for (mod_name, m) in modules { for def in &m.defs { @@ -544,7 +550,10 @@ fn build_registry( class_by_name.insert(c.name.clone(), c); } Def::Type(t) => { - type_def_module.insert(t.name.clone(), mod_name.clone()); + type_def_module.insert( + (mod_name.clone(), t.name.clone()), + mod_name.clone(), + ); } _ => {} } @@ -654,7 +663,7 @@ fn build_registry( .cloned() .unwrap_or_else(|| "".into()); let type_mod = type_def_module - .get(&type_repr) + .get(&(mod_name.clone(), type_repr.clone())) .cloned() .unwrap_or_else(|| "".into()); let coherent = mod_name == &class_mod || mod_name == &type_mod; @@ -676,7 +685,11 @@ fn build_registry( // declaration from elsewhere produce the same key (both // refer to the same type under the canonical-form rule). let type_hash = canonical::type_hash( - &normalize_type_for_registry(&inst.type_, &type_def_module), + &normalize_type_for_registry( + mod_name, + &inst.type_, + &type_def_module, + ), ); let key = (inst.class.clone(), type_hash); if let Some(prior) = entries.get(&key) { @@ -878,18 +891,26 @@ fn is_primitive_type_name(name: &str) -> bool { /// The qualifier is the type's *defining* module (looked up in /// `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 +/// 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, + 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(name) { + } 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 @@ -902,17 +923,17 @@ fn normalize_type_for_registry( name: new_name, args: args .iter() - .map(|a| normalize_type_for_registry(a, type_def_module)) + .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(p, type_def_module)) + .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(ret, type_def_module)), + ret: Box::new(normalize_type_for_registry(caller_module, ret, type_def_module)), ret_mode: *ret_mode, effects: effects.clone(), }, @@ -922,10 +943,10 @@ fn normalize_type_for_registry( .iter() .map(|c| crate::ast::Constraint { class: c.class.clone(), - type_: normalize_type_for_registry(&c.type_, type_def_module), + type_: normalize_type_for_registry(caller_module, &c.type_, type_def_module), }) .collect(), - body: Box::new(normalize_type_for_registry(body, type_def_module)), + body: Box::new(normalize_type_for_registry(caller_module, body, type_def_module)), }, Type::Var { name } => Type::Var { name: name.clone() }, } @@ -2522,8 +2543,14 @@ mod tests { /// guards against at the outer level. #[test] fn ct1_5a_normalize_recurses_into_forall_constraints() { - let mut type_def_module: BTreeMap = BTreeMap::new(); - type_def_module.insert("MyInt".to_string(), "other".to_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(), + ); // Forall a. (TShow MyInt) => a -> a // The constraint's type carries a bare `MyInt` that should be @@ -2546,7 +2573,7 @@ mod tests { }), }; - let out = normalize_type_for_registry(&input, &type_def_module); + let out = normalize_type_for_registry("caller", &input, &type_def_module); match out { Type::Forall { constraints, .. } => { diff --git a/crates/ailang-core/tests/ctt2_registry_rekey.rs b/crates/ailang-core/tests/ctt2_registry_rekey.rs new file mode 100644 index 0000000..e56c09d --- /dev/null +++ b/crates/ailang-core/tests/ctt2_registry_rekey.rs @@ -0,0 +1,70 @@ +//! 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` 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::>() + ); + assert!( + ws.registry.entries.contains_key(&lib_key), + "registry missing entry for (MyC, ctt2_collision_lib.Foo); entries: {:?}", + ws.registry.entries.keys().collect::>() + ); + + // 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" + ); +} diff --git a/docs/journals/2026-05-12-iter-ctt.2.md b/docs/journals/2026-05-12-iter-ctt.2.md new file mode 100644 index 0000000..4653bbc --- /dev/null +++ b/docs/journals/2026-05-12-iter-ctt.2.md @@ -0,0 +1,142 @@ +# iter ctt.2 — `Registry.type_def_module` re-key to tuple-keyed map + +**Date:** 2026-05-12 +**Started from:** 548ebf8d51758ab9f156b266e0baa59c78b14034 +**Status:** DONE +**Tasks completed:** 3 of 3 + +## Summary + +`Registry.type_def_module` re-keyed from `BTreeMap` to +`BTreeMap<(String, String), String>` keyed by `(owning_module, +bare_name)`. A new `caller_module: &str` parameter threads through +`normalize_type_for_registry` and `Registry::normalize_type_for_lookup`, +and all four `ailang-check` consumer sites pass the correct +module-context per the plan's Design Notes block. A regression test +plus a three-module fixture exercise the bare-name collision the +re-key fixes — two modules each declaring `type Foo` now register +under distinct canonical keys instead of tripping the silent-overwrite +defect. `cargo test --workspace` green (16 binary-test suites, +~600 tests total, zero failures). + +## Per-task notes + +- iter ctt.2.1: RED-first — three fixture JSON files + (`ctt2_collision_{cls,lib,main}.ail.json`) plus + `crates/ailang-core/tests/ctt2_registry_rekey.rs`. Test goes + RED today. Failure shape was `OrphanInstance` rather than the + plan's predicted `DuplicateInstance` (a) or missing-hash (b) — + see Concerns. +- iter ctt.2.2: GREEN — atomic re-key edit pass. Touched the field + type + doc-comment, `Registry::normalize_type_for_lookup` + signature + body, pass-1 declaration + insert, + pass-2 coherence-check lookup + canonical-form hashing call, + `normalize_type_for_registry` signature + doc-comment + body + (recursive calls thread caller_module everywhere), the inline + `ct1_5a_normalize_recurses_into_forall_constraints` test + at workspace.rs:2543, and all four `ailang-check` consumer sites + at the module-context dictated by the plan's Design Notes + (lib.rs:1640 → `env.current_module.as_str()`, mono.rs:121 → + `defining_module.as_str()`, mono.rs:624 and :1175 → `module_name`). + Clean `cargo build --workspace` at the end of the pass. +- iter ctt.2.3: workspace-wide test gate. New test goes GREEN. + Full `cargo test --workspace` green, no test in any crate + regressed. The plan's Step 2 hypothesis ("a fixture under + `examples/` might legitimately exercise a bare cross-module + type reference") did not materialise — the existing corpus + uses canonical-form (qualified) cross-module type refs + exclusively, so the tightening had no surface. + +## Concerns + +- Task 1 RED-failure shape diverged from the plan's predicted + shapes (a) `DuplicateInstance` and (b) missing-`contains_key`. + The plan's verbatim two-module fixture pair (lib imports main + + main imports lib + lib-side instance with qualified class + `ctt2_collision_main.MyC`) had two independent structural + defects against the loaded codebase invariants: + 1. The workspace loader rejects import cycles + (`WorkspaceLoadError::Cycle`) at module-name granularity — + the plan's claim "Two-way import is supported; workspace + loader DFS handles cycles at module-name granularity" is + empirically false. Confirmed by inspection at + `workspace.rs:1410-1413` and `:1451-1454`. + 2. `validate_canonical_type_names` rejects qualified + `InstanceDef.class` with `QualifiedClassName` (see test + `ct1_validator_rejects_qualified_instancedef_class` at + workspace.rs:2321) — the plan's lib-side instance with + `class: "ctt2_collision_main.MyC"` would have been rejected + even without the cycle. + + Implementer-phase repair: introduced a third module + `ctt2_collision_cls.ail.json` carrying `class MyC`, broke the + cycle (both lib and main import cls only; main also imports + lib so the loader DFS reaches it), and dropped the qualifier + from both instance heads (both declare bare `class: "MyC"` + satisfying coherence via the type's module). The plan's + *core* RED-condition is preserved: two modules each declaring + `type Foo` with `instance MyC Foo` collide on the pre-ctt.2 + bare-name `type_def_module` key. + + The actually-observed RED shape was `OrphanInstance` rather + than `DuplicateInstance` because the pass-2 coherence check + (`workspace.rs:656-659`) uses the bare-name lookup *before* + the duplicate-uniqueness check fires — lib's `Foo` and main's + `Foo` both resolve to whichever module won the alphabetical + race (`ctt2_collision_main` beats `ctt2_collision_lib` + lexicographically), so lib's instance is no longer coherent + ("type lives in main, class lives in cls, instance lives in + lib — neither matches"). The re-key fixes this consumer site + simultaneously with the duplicate-check site because both go + through the same `type_def_module` lookup, now tuple-keyed. + + The deviation from the plan's verbatim fixtures is an + implementer-phase NEEDS_CONTEXT repair that the + user-no-stop-instruction permitted in-line; the plan's *intent* + (RED-first against the bare-name collision) is achieved and + the GREEN-state matches the plan's acceptance criteria + unchanged. Boss may want to back-edit the plan or amend the + spec to note the multi-module fixture shape if this kind of + collision regression returns later. + +## Known debt + +- The `OrphanInstance` consumer site at `workspace.rs:656-659` + is fixed by ctt.2 but the diagnostic-shape change (orphan + instead of duplicate-on-loser) is not separately tested. The + new test asserts the GREEN state directly (both entries land + under distinct keys) which transitively exercises both + consumer sites, but neither the orphan nor the duplicate + diagnostic shapes are pinned by a dedicated RED test. Adding + one is plausibly out-of-scope for ctt.2. + +## Files touched + +Modified (3): +- `crates/ailang-check/src/lib.rs` — single call-site update at + the NoInstance check inside `check_fn`. +- `crates/ailang-check/src/mono.rs` — three call-site updates + (monomorphise_workspace at :121, collect_mono_targets at :624, + collect_residuals_ordered at :1175). +- `crates/ailang-core/src/workspace.rs` — field re-key, + `Registry::normalize_type_for_lookup` signature, + `normalize_type_for_registry` signature + body, + pass-1 declaration + insert, + pass-2 coherence-check lookup + canonical-form hashing call, + inline test `ct1_5a_normalize_recurses_into_forall_constraints` + at :2543; doc-comments at field-decl and at both + `normalize_…` fns. + +Added (4): +- `crates/ailang-core/tests/ctt2_registry_rekey.rs` — regression test. +- `examples/ctt2_collision_cls.ail.json` — class-bearing module + (not in plan; required to break the cycle in the plan's + verbatim fixture design). +- `examples/ctt2_collision_lib.ail.json` — `type Foo = MkLib` + + bare `instance MyC Foo`. +- `examples/ctt2_collision_main.ail.json` — `type Foo = MkMain` + + bare `instance MyC Foo`, plus imports of both `cls` and `lib`. + +## Stats + +bench/orchestrator-stats/2026-05-12-iter-ctt.2.json diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index 4b18496..340ccc0 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -38,3 +38,4 @@ - 2026-05-12 — iter eob.1: Effect-op args walked as Borrow (uniqueness.rs + linearity.rs + linearity.rs doc-comment); heap-Str RC discipline closes (int_to_str / float_to_str `ret_mode: Implicit` → `Own` at 4 lockstep sites across builtins.rs + synth.rs; `drop_symbol_for_binder` App-arm gets `Str` carve-out symmetric to `field_drop_call`'s existing one); 2 pre-existing RED tests at e2e.rs (commit 592d87b) flipped to GREEN unchanged with predicted concrete numbers (`allocs==1,frees==1,live==0` and `allocs==2,frees==2,live==0`); 2 new positive tests + fixtures (primitive-Int through `io/print_int`: zero RC traffic; heap-Str repeated borrow through 2× `io/print_str`: `allocs==1,frees==1,live==0`); DESIGN.md §Decision 10 anchors both arg-position rules together (Ctor=Consume, Do=Borrow, plus a paragraph linking Do=Borrow to ret_mode==Own letbinder-trackability); WhatsNew entry shipped, roadmap P1 `[milestone] Heap-Str ABI` checked off, Post-22-Prelude `depends on:` line removed; 7/7 tasks first-shot, zero review re-loops; lint side-effect surface (over-strict-mode on (own T) params used solely via effect-ops) was empty for the current corpus → 2026-05-12-iter-eob.1.md - 2026-05-12 — audit-eob: milestone close (heap-str-abi) — architect drift fixed inline as `eob.tidy` (3 DESIGN.md edits: float_to_str / int_to_str caveats dropped, "Str ABI" anchor added documenting both heap-Str and static-Str realisations with their shared consumer ABI and codegen-level non-RC invariant for static-Str); bench mixed (latency.explicit_at_rc improvement cluster reappears for the 3rd consecutive audit + new marginal bump_s regressions on list_sum/hof_pipeline 12% over 10% tol), baseline pristine for 3rd consecutive audit (conservative call: latency cluster has no identified cause across 3 audits — ratify-without-attribution would obscure future signal; bump_s cluster is first-sighting, observe next audit) → 2026-05-12-audit-eob.md - 2026-05-12 — iter ctt.1: env-overlay shape ratification — new DESIGN.md top-level section `## Env construction` anchors the `env.types` (owning) / `env.ctor_index` (reverse-index) split with the semantic rationale, plus the intentional check-side / mono-side asymmetry (mono-side narrowed at ct.3.2, check-side retains both halves because in-band `DuplicateCtor` consumes the per-module `env.ctor_index` rebuild); new behavioural-pin test `crates/ailang-check/tests/duplicate_ctor_pin.rs` ratifies the consumer; two P2 roadmap todos struck `[x]` (overlay shape question, per-module overlay narrowing); 3/3 tasks first-shot, zero review re-loops, no production-code edits → 2026-05-12-iter-ctt.1.md +- 2026-05-12 — iter ctt.2: `Registry.type_def_module` re-key from `BTreeMap` to `BTreeMap<(String, String), String>` keyed by `(owning_module, bare_name)`; new `caller_module: &str` parameter threads through `normalize_type_for_registry` + `Registry::normalize_type_for_lookup`; four `ailang-check` consumer sites (lib.rs:1640, mono.rs:121/624/1175) pass the correct module-context (env.current_module / defining_module / module_name) per the plan's Design Notes; new regression test plus three-module fixture (`ctt2_collision_{cls,lib,main}.ail.json`) exercises the cross-module bare-name collision shape that pre-ctt.2 silently overwrote; RED-failure observed as `OrphanInstance` rather than the plan's predicted `DuplicateInstance` because the pass-2 coherence check (workspace.rs:656-659) is also a bare-name consumer and fires earlier; both consumers fixed by the same re-key; plan's verbatim two-module fixture had two structural defects (two-way imports → Cycle, qualified `InstanceDef.class` → QualifiedClassName) that the implementer-phase repair handled by introducing the third cls-module; spec-intent (RED-first against bare-name collision) preserved; `cargo test --workspace` green (16 binary-test suites, ~600 tests, zero failures) → 2026-05-12-iter-ctt.2.md diff --git a/examples/ctt2_collision_cls.ail.json b/examples/ctt2_collision_cls.ail.json new file mode 100644 index 0000000..11f9669 --- /dev/null +++ b/examples/ctt2_collision_cls.ail.json @@ -0,0 +1,23 @@ +{ + "schema": "ailang/v0", + "name": "ctt2_collision_cls", + "imports": [], + "defs": [ + { + "kind": "class", + "name": "MyC", + "param": "a", + "methods": [ + { + "name": "op", + "type": { + "k": "fn", + "params": [{ "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Int" }, + "effects": [] + } + } + ] + } + ] +} diff --git a/examples/ctt2_collision_lib.ail.json b/examples/ctt2_collision_lib.ail.json new file mode 100644 index 0000000..0edb4ab --- /dev/null +++ b/examples/ctt2_collision_lib.ail.json @@ -0,0 +1,29 @@ +{ + "schema": "ailang/v0", + "name": "ctt2_collision_lib", + "imports": [{ "module": "ctt2_collision_cls" }], + "defs": [ + { + "kind": "type", + "name": "Foo", + "ctors": [{ "name": "MkLib", "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": 2 } } + } + } + ] + } + ] +} diff --git a/examples/ctt2_collision_main.ail.json b/examples/ctt2_collision_main.ail.json new file mode 100644 index 0000000..c6091ad --- /dev/null +++ b/examples/ctt2_collision_main.ail.json @@ -0,0 +1,32 @@ +{ + "schema": "ailang/v0", + "name": "ctt2_collision_main", + "imports": [ + { "module": "ctt2_collision_cls" }, + { "module": "ctt2_collision_lib" } + ], + "defs": [ + { + "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 } } + } + } + ] + } + ] +}