iter ctt.2: Registry.type_def_module re-key to (owning_module, bare_name)
This commit is contained in:
@@ -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."
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -118,7 +118,9 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
|
||||
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 {
|
||||
|
||||
@@ -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<String, 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>,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
/// 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.
|
||||
/// 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.
|
||||
///
|
||||
/// 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<String, String> = BTreeMap::new();
|
||||
let mut type_def_module: BTreeMap<String, String> = BTreeMap::new();
|
||||
let mut type_def_module: BTreeMap<(String, String), String> = BTreeMap::new();
|
||||
let mut class_by_name: BTreeMap<String, &ClassDef> = 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(|| "<unknown-class>".into());
|
||||
let type_mod = type_def_module
|
||||
.get(&type_repr)
|
||||
.get(&(mod_name.clone(), type_repr.clone()))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "<primitive-or-unknown>".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<String, String>,
|
||||
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<String, String> = 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, .. } => {
|
||||
|
||||
@@ -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<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"
|
||||
);
|
||||
}
|
||||
@@ -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<String, String>` 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
|
||||
@@ -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<String, String>` 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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 } }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user