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.
41 KiB
Iter mq.1 — Canonical-form extension for class-ref fields — Implementation Plan
Parent spec:
docs/specs/0023-module-qualified-class-names.mdFor agentic workers: REQUIRED SUB-SKILL: use
skills/implementto run this plan. Steps use- [ ]checkboxes for tracking.
Goal: Move the three class-reference schema fields (InstanceDef.class,
Constraint.class, SuperclassRef.class) from bare-only to canonical
form (bare for same-module, <module>.<Class> for cross-module), extend
ct.1's validate_canonical_type_names validator to enforce the new
rule, qualify all workspace-internal class-name keys (class_def_module,
class_by_name, registry entries, ClassMethodEntry.class_name,
class_superclasses), and adjust test assertions that string-compare
against bare class names. Dispatch and MethodNameCollision are
unchanged in this iter.
Architecture: Symmetric application of ct.1's existing rule to three
class-ref fields. The existing check_class_name_fields function (which
rejects . in all four fields today) is narrowed to ClassDef.name
only; the three migrated fields get new BareCrossModuleClassRef
diagnostic walks inside validate_canonical_type_names. Class-key
qualification flows through the registry construction and the
ailang-check ClassMethodEntry source insert site; all downstream
consumers (synth Var-arm, mono targets, discharge) read the qualified
strings transparently because they only compare and look up by exact
string match.
Tech Stack: ailang-core (workspace.rs, ast.rs),
ailang-check (lib.rs), ail CLI (main.rs Display arm),
crates/ail/tests/typeclass_22b{2,3}.rs (assertion-string updates).
Pre-flight verification of recon-flagged spec cost overstatement: the
spec's §"Known costs"-Iter-1 estimates 14 test fixtures migrated. Recon
confirms every existing class-ref field in examples/*.ail.json
references a class declared in the same fixture file (intra-module).
Under the canonical-form rule, intra-module refs stay bare, so existing
fixture content carries zero diff. Plan does NOT add work to "migrate"
fixtures that need no migration; a verification step in Task 7
confirms zero existing-fixture diff. New cross-module fixtures land
only where Task 3 needs them for RED-first validator pin tests.
Task 1: Add BareCrossModuleClassRef + BadCrossModuleClassRef diagnostic variants + Display arms
Files:
- Modify:
crates/ailang-core/src/workspace.rs:341(add two sibling variants, immediately afterBareCrossModuleTypeRef/BadCrossModuleTypeRef) - Modify:
crates/ail/src/main.rs:1285(two Display arms) - Test: inline in
crates/ailang-core/src/workspace.rs#[cfg(test)] mod tests
The two new variants are siblings of the existing
BareCrossModuleTypeRef (workspace.rs:341) and
BadCrossModuleTypeRef (workspace.rs:355) for class-ref fields:
BareCrossModuleClassRef fires when a bare class ref does not
resolve to a local class of the owning module; BadCrossModuleClassRef
fires when a qualified class ref's owner is not a known module or
declares no class by that name.
- Step 1: Write the RED test for both new variants' Display output
Append inside the existing mod tests block in crates/ailang-core/src/workspace.rs (after the existing BareCrossModuleTypeRef-Display test if any; otherwise at the end of the test module):
/// mq.1.1: `BareCrossModuleClassRef` is the sibling diagnostic for
/// bare cross-module class references on `InstanceDef.class`,
/// `Constraint.class`, and `SuperclassRef.class`. Verifies the
/// variant is constructible and its Display surface names the
/// "class" wording (vs. "type" in the sibling variant).
#[test]
fn mq1_bare_cross_module_class_ref_display_names_class() {
let err = WorkspaceLoadError::BareCrossModuleClassRef {
module: "user".to_string(),
name: "Show".to_string(),
candidates: vec!["prelude.Show".to_string()],
};
let rendered = format!("{}", err);
assert!(rendered.contains("class"), "Display must name 'class' wording, got: {rendered}");
assert!(rendered.contains("Show"), "Display must echo the offending class name, got: {rendered}");
assert!(rendered.contains("prelude.Show"), "Display must list candidates, got: {rendered}");
}
/// mq.1.1b: `BadCrossModuleClassRef` is the sibling diagnostic for
/// qualified class references whose owner module is unknown or
/// declares no class by that name. Verifies the variant is
/// constructible and its Display surface names the offending
/// qualified form.
#[test]
fn mq1_bad_cross_module_class_ref_display_names_qualified() {
let err = WorkspaceLoadError::BadCrossModuleClassRef {
module: "user".to_string(),
name: "unknownlib.Show".to_string(),
};
let rendered = format!("{}", err);
assert!(rendered.contains("unknownlib.Show"), "Display must echo the qualified form, got: {rendered}");
assert!(rendered.contains("class") || rendered.contains("module"),
"Display must mention class or module context, got: {rendered}");
}
- Step 2: Run tests to verify they fail
Run: cargo test --workspace -p ailang-core mq1_bare_cross_module_class_ref_display_names_class mq1_bad_cross_module_class_ref_display_names_qualified
Expected: compile error (the new variants do not yet exist).
- Step 3: Add the two variants to
WorkspaceLoadError
Insert immediately after the existing BareCrossModuleTypeRef variant
at crates/ailang-core/src/workspace.rs:341-345. Two sibling variants
with the same field shapes as the corresponding type-ref variants:
/// mq.1 (canonical-class-names): a class-reference field
/// (`InstanceDef.class`, `Constraint.class`, or
/// `SuperclassRef.class`) carries a bare name that does not resolve
/// to a local class of the owning module. Under the canonical-form
/// rule extended in mq.1, bare = local-class-of-owning-module; a
/// bare cross-module class reference is a schema violation.
/// `candidates` lists the qualified forms found by scanning the
/// owning module's imports for matching class declarations.
#[error(
"module `{module}` contains bare class name `{name}` that does not resolve to a local class. \
AILang's `.ail.json` requires cross-module class references to be qualified. \
Candidates from imports: {candidates:?}."
)]
BareCrossModuleClassRef {
module: String,
name: String,
candidates: Vec<String>,
},
/// mq.1 (canonical-class-names): a qualified class reference of
/// the form `<owner>.<class>` was encountered, but `<owner>` is
/// not a known module in the workspace, or `<owner>` is known
/// but declares no class by that name. Sibling of
/// `BadCrossModuleTypeRef` for class references.
#[error(
"module `{module}` references qualified class `{name}` but the owner module is not known \
or does not declare a class by that name"
)]
BadCrossModuleClassRef {
module: String,
name: String,
},
- Step 4: Add the two Display arms in the CLI
Open crates/ail/src/main.rs. Find the existing W::BareCrossModuleTypeRef arm (the one that emits the structured-diagnostic JSON with "code" field). Add a sibling arm immediately after it:
W::BareCrossModuleClassRef { module, name, candidates } => {
let mut obj = serde_json::json!({
"code": "bare-cross-module-class-ref",
"module": module,
"name": name,
"candidates": candidates,
});
println!("{}", obj);
}
W::BadCrossModuleClassRef { module, name } => {
let mut obj = serde_json::json!({
"code": "bad-cross-module-class-ref",
"module": module,
"name": name,
});
println!("{}", obj);
}
(If the existing arm uses a non-JSON Display path, mirror that path
exactly. The implementer reads the existing arms for
W::BareCrossModuleTypeRef and W::BadCrossModuleTypeRef at
crates/ail/src/main.rs:1201-ish and matches their shapes verbatim.)
- Step 5: Run tests to verify both pass
Run: cargo test --workspace -p ailang-core mq1_bare_cross_module_class_ref_display_names_class mq1_bad_cross_module_class_ref_display_names_qualified
Expected: both PASS.
- Step 6: Confirm no other test broke
Run: cargo test --workspace
Expected: green (only an additive variant; no semantic change yet).
Task 2: Doc-comments on the three migrated class-ref fields + ClassDef.name
Files:
-
Modify:
crates/ailang-core/src/ast.rs:260-273(ClassDef.name doc) -
Modify:
crates/ailang-core/src/ast.rs:277-284(SuperclassRef.class doc) -
Modify:
crates/ailang-core/src/ast.rs:310-323(InstanceDef.class doc) -
Modify:
crates/ailang-core/src/ast.rs:343-349(Constraint.class doc) -
Step 1: Update
ClassDef.namedoc-comment
In crates/ailang-core/src/ast.rs:260-273, find the existing
/// Class name (capitalised by convention). line above
pub name: String. Replace with:
/// Class name (capitalised by convention). Bare — symmetric to
/// `TypeDef.name`, the field is the defining-site context, not
/// a reference. Cross-module class references live in
/// `InstanceDef.class`, `Constraint.class`, and
/// `SuperclassRef.class`; those fields carry the canonical form
/// (bare for same-module, `<module>.<Class>` for cross-module)
/// per mq.1.
pub name: String,
- Step 2: Update
SuperclassRef.classdoc-comment
In crates/ailang-core/src/ast.rs:277-284, replace the
/// Superclass name. line above pub class: String with:
/// Superclass name in canonical form (mq.1): bare for a
/// same-module class, `<module>.<Class>` for a cross-module
/// class. Symmetric to ct.1's `Type::Con.name` rule.
pub class: String,
- Step 3: Update
InstanceDef.classdoc-comment
In crates/ailang-core/src/ast.rs:312-317, replace the
/// Class being instantiated. line above pub class: String with:
/// Class being instantiated, in canonical form (mq.1): bare for
/// a same-module class, `<module>.<Class>` for a cross-module
/// class. Symmetric to ct.1's `Type::Con.name` rule.
pub class: String,
- Step 4: Update
Constraint.classdoc-comment
In crates/ailang-core/src/ast.rs:343-349, replace the
/// Class name. line above pub class: String with:
/// Class name in canonical form (mq.1): bare for a same-module
/// class, `<module>.<Class>` for a cross-module class.
/// Symmetric to ct.1's `Type::Con.name` rule.
pub class: String,
- Step 5: Run
cargo test --workspace
Run: cargo test --workspace
Expected: green. Doc-only changes carry zero semantic effect.
Task 3: Extend validate_canonical_type_names for the three class-ref fields + invert existing rejection
Files:
-
Create:
examples/mq1_xmod_constraint_class.ail.json(positive cross-module fixture, two-module pair) -
Create:
examples/mq1_xmod_constraint_class_dep.ail.json(the dependency module) -
Modify:
crates/ailang-core/src/workspace.rs:921-964(validate_canonical_type_names) -
Modify:
crates/ailang-core/src/workspace.rs:1282-1341(check_class_name_fieldsnarrows to ClassDef.name only) -
Modify:
crates/ailang-core/src/workspace.rs:2284-2377(4 existing pin tests inverted: instance, constraint-on-fn, constraint-on-class-method, superclass) -
Modify:
crates/ailang-core/src/workspace.rs:2618-2632(on-disk fixture pin test inverted) -
Test: inline in
crates/ailang-core/src/workspace.rsmod tests(3 RED-first pin tests forBareCrossModuleClassRef, 1 positive cross-module pin test) -
Step 1: Write three RED-first pin tests for
BareCrossModuleClassRef
Append at the end of the existing mod tests block in
crates/ailang-core/src/workspace.rs:
/// mq.1.3: `InstanceDef.class` carrying a bare name that does NOT
/// resolve to a local class of the owning module must fire
/// `BareCrossModuleClassRef`. The fixture imports a sibling module
/// that declares the class under the bare name, so the candidate
/// list is non-empty.
#[test]
fn mq1_bare_xmod_instancedef_class_fires() {
let mut modules = BTreeMap::new();
// Owning module B: instance Show Int (bare class ref, but Show
// is not defined in B).
modules.insert(
"B".to_string(),
Module {
name: "B".to_string(),
imports: vec!["A".to_string()],
defs: vec![Def::Instance(InstanceDef {
class: "Show".to_string(), // <- bare, cross-module
type_: Type::Con { name: "Int".to_string(), args: vec![] },
methods: vec![],
doc: None,
})],
},
);
// Dep module A: declares class Show
modules.insert(
"A".to_string(),
Module {
name: "A".to_string(),
imports: vec![],
defs: vec![Def::Class(ClassDef {
name: "Show".to_string(),
param: "a".to_string(),
superclass: None,
methods: vec![],
doc: None,
})],
},
);
let err = validate_canonical_type_names(&modules).expect_err("expected reject");
match err {
WorkspaceLoadError::BareCrossModuleClassRef { module, name, candidates } => {
assert_eq!(module, "B");
assert_eq!(name, "Show");
assert!(candidates.contains(&"A.Show".to_string()), "candidates: {candidates:?}");
}
other => panic!("expected BareCrossModuleClassRef, got {other:?}"),
}
}
/// mq.1.3: `Constraint.class` on a `Def::Fn` carrying a bare name
/// that does not resolve to a local class fires
/// `BareCrossModuleClassRef`.
#[test]
fn mq1_bare_xmod_constraint_class_on_fn_fires() {
let mut modules = BTreeMap::new();
modules.insert(
"B".to_string(),
Module {
name: "B".to_string(),
imports: vec!["A".to_string()],
defs: vec![Def::Fn(FnDef {
name: "f".to_string(),
ty: Type::Forall {
vars: vec!["a".to_string()],
constraints: vec![Constraint {
class: "Show".to_string(), // <- bare, cross-module
type_: Type::Var { name: "a".to_string() },
}],
body: Box::new(Type::Fn {
params: vec![Type::Var { name: "a".to_string() }],
param_modes: vec![ParamMode::Move],
ret: Box::new(Type::Con { name: "Unit".to_string(), args: vec![] }),
effects: vec![],
}),
},
params: vec!["x".to_string()],
body: Term::Var { name: "x".to_string() },
doc: None,
})],
},
);
modules.insert(
"A".to_string(),
Module {
name: "A".to_string(),
imports: vec![],
defs: vec![Def::Class(ClassDef {
name: "Show".to_string(),
param: "a".to_string(),
superclass: None,
methods: vec![],
doc: None,
})],
},
);
let err = validate_canonical_type_names(&modules).expect_err("expected reject");
match err {
WorkspaceLoadError::BareCrossModuleClassRef { module, name, .. } => {
assert_eq!(module, "B");
assert_eq!(name, "Show");
}
other => panic!("expected BareCrossModuleClassRef, got {other:?}"),
}
}
/// mq.1.3: `SuperclassRef.class` on a `Def::Class` carrying a bare
/// name that does not resolve to a local class fires
/// `BareCrossModuleClassRef`.
#[test]
fn mq1_bare_xmod_superclassref_class_fires() {
let mut modules = BTreeMap::new();
modules.insert(
"B".to_string(),
Module {
name: "B".to_string(),
imports: vec!["A".to_string()],
defs: vec![Def::Class(ClassDef {
name: "MyOrd".to_string(),
param: "a".to_string(),
superclass: Some(SuperclassRef {
class: "Eq".to_string(), // <- bare, cross-module
type_: "a".to_string(),
}),
methods: vec![],
doc: None,
})],
},
);
modules.insert(
"A".to_string(),
Module {
name: "A".to_string(),
imports: vec![],
defs: vec![Def::Class(ClassDef {
name: "Eq".to_string(),
param: "a".to_string(),
superclass: None,
methods: vec![],
doc: None,
})],
},
);
let err = validate_canonical_type_names(&modules).expect_err("expected reject");
match err {
WorkspaceLoadError::BareCrossModuleClassRef { module, name, .. } => {
assert_eq!(module, "B");
assert_eq!(name, "Eq");
}
other => panic!("expected BareCrossModuleClassRef, got {other:?}"),
}
}
- Step 2: Write the positive cross-module pin test
Append in the same mod tests block:
/// mq.1.3: a qualified `InstanceDef.class` referencing a class in
/// an imported module is the canonical form post-mq.1 and is
/// accepted by `validate_canonical_type_names`. Symmetric to ct.1's
/// "qualified Type::Con resolves" positive path.
#[test]
fn mq1_qualified_instancedef_class_accepted() {
let mut modules = BTreeMap::new();
modules.insert(
"B".to_string(),
Module {
name: "B".to_string(),
imports: vec!["A".to_string()],
defs: vec![Def::Instance(InstanceDef {
class: "A.Show".to_string(), // <- qualified, cross-module
type_: Type::Con { name: "Int".to_string(), args: vec![] },
methods: vec![],
doc: None,
})],
},
);
modules.insert(
"A".to_string(),
Module {
name: "A".to_string(),
imports: vec![],
defs: vec![Def::Class(ClassDef {
name: "Show".to_string(),
param: "a".to_string(),
superclass: None,
methods: vec![],
doc: None,
})],
},
);
validate_canonical_type_names(&modules).expect("qualified A.Show must be accepted");
}
- Step 3: Run the four new tests to confirm RED
Run:
cargo test --workspace -p ailang-core mq1_bare_xmod_instancedef_class_fires \
mq1_bare_xmod_constraint_class_on_fn_fires \
mq1_bare_xmod_superclassref_class_fires \
mq1_qualified_instancedef_class_accepted
Expected: the three bare tests panic with "expected BareCrossModuleClassRef, got QualifiedClassName" (because check_class_name_fields still rejects bare under no current rule — actually it accepts bare and rejects qualified, so the bare tests will likely panic with "expected reject" because no error fires); the qualified test panics because check_class_name_fields rejects qualified forms.
- Step 4: Implement the three new field walks in
validate_canonical_type_names
In crates/ailang-core/src/workspace.rs, locate the per-def loop at
~line 945-960 inside validate_canonical_type_names. Add three new
walks calling a shared helper (or inlining the three-rule check from
check_type_con_name at workspace.rs:968-1028).
The helper signature:
/// mq.1: apply the canonical-form rule to a class-reference field.
/// Same three-rule structure as `check_type_con_name` for type refs:
/// qualified known-module ⇒ accept; bare same-module-class ⇒ accept;
/// bare cross-module ⇒ fire `BareCrossModuleClassRef` with candidates
/// scanned from the owning module's imports.
fn check_class_ref(
owning_module: &str,
class_ref: &str,
class_def_module: &BTreeMap<String, String>, // bare-class -> defining-module
imports: &[String],
field_label: &'static str,
) -> Result<(), WorkspaceLoadError> {
if let Some((owner, bare)) = class_ref.split_once('.') {
// Qualified form: owner must be a known module that declares the class.
if !class_def_module.iter().any(|(k, m)| m == owner && k == bare) {
return Err(WorkspaceLoadError::BadCrossModuleClassRef {
module: owning_module.to_string(),
name: class_ref.to_string(),
});
}
Ok(())
} else {
// Bare form: must resolve to a class in the owning module.
let defining = class_def_module.get(class_ref);
match defining {
Some(m) if m == owning_module => Ok(()),
_ => {
// Cross-module bare: list qualified candidates from imports.
let candidates: Vec<String> = imports
.iter()
.filter_map(|imp| {
if class_def_module.get(class_ref).map(|m| m == imp).unwrap_or(false) {
Some(format!("{imp}.{class_ref}"))
} else {
None
}
})
.collect();
Err(WorkspaceLoadError::BareCrossModuleClassRef {
module: owning_module.to_string(),
name: class_ref.to_string(),
candidates,
})
}
}
}
}
Note: BadCrossModuleClassRef is the sibling of BadCrossModuleTypeRef
(workspace.rs:355). The implementer adds this variant to the enum in
the same step (insert immediately after BadCrossModuleTypeRef),
sibling Display arm in main.rs if not auto-Display-derived.
Build the class_def_module: BTreeMap<String, String> (bare-class →
defining-module) once at the top of validate_canonical_type_names by
walking every module's Def::Class entries, then pass it into the
helper.
Add the three call-site walks inside the existing per-def loop:
for def in &m.defs {
// ...existing Type::Con walks...
match def {
Def::Instance(id) => {
check_class_ref(mod_name, &id.class, &class_def_module, &m.imports, "InstanceDef.class")?;
}
Def::Class(cd) => {
if let Some(sc) = &cd.superclass {
check_class_ref(mod_name, &sc.class, &class_def_module, &m.imports, "SuperclassRef.class")?;
}
for cmth in &cd.methods {
if let Type::Forall { constraints, .. } = &cmth.ty {
for c in constraints {
check_class_ref(mod_name, &c.class, &class_def_module, &m.imports, "Constraint.class")?;
}
}
}
}
Def::Fn(fd) => {
if let Type::Forall { constraints, .. } = &fd.ty {
for c in constraints {
check_class_ref(mod_name, &c.class, &class_def_module, &m.imports, "Constraint.class")?;
}
}
}
_ => {}
}
}
- Step 5: Narrow
check_class_name_fieldstoClassDef.name-only
In crates/ailang-core/src/workspace.rs:1287-1341, delete the three
rejection arms for SuperclassRef.class, InstanceDef.class,
Constraint.class (lines 1307-1320, 1323-1328, 1329-1338). Keep only
the ClassDef.name arm at 1303-1306. The function body becomes:
fn check_class_name_fields(
def: &Def,
owning_module: &str,
) -> Result<(), WorkspaceLoadError> {
if let Def::Class(cd) = def {
if cd.name.contains('.') {
return Err(WorkspaceLoadError::QualifiedClassName {
module: owning_module.to_string(),
name: cd.name.clone(),
field: "ClassDef.name",
});
}
}
Ok(())
}
Update the function doc-comment to reflect the post-mq.1 scope.
- Step 6: Invert the four in-test pin tests that rejected qualified class-ref forms
In crates/ailang-core/src/workspace.rs, locate the existing tests at
2284-2377 (three tests at 2284, 2312, 2341, plus the
classdef-method-constraint test at 2541). For each of the four tests
that today asserts QualifiedClassName on a qualified class-ref field
in InstanceDef.class / Constraint.class / SuperclassRef.class,
invert the assertion: the qualified form is now ACCEPTED. Replace the
panic-match block with:
validate_canonical_type_names(&modules)
.expect("qualified <field> is the canonical form post-mq.1");
Update each test's doc-comment to reflect the inverted intent. The test
at 2255 (ClassDef.name-rejects-qualified) stays unchanged.
- Step 7: Invert the on-disk fixture pin test at 2618-2632
If the on-disk fixture test at workspace.rs:2618 covers a qualified-form
rejection on one of the three migrated fields, invert it the same way
as Step 6 (expect-accept post-mq.1). If it covers ClassDef.name
specifically, it stays unchanged. The implementer checks the test body
to determine which case.
- Step 8: Run the four mq.1.3 tests to confirm GREEN
Run:
cargo test --workspace -p ailang-core mq1_bare_xmod_instancedef_class_fires \
mq1_bare_xmod_constraint_class_on_fn_fires \
mq1_bare_xmod_superclassref_class_fires \
mq1_qualified_instancedef_class_accepted
Expected: all four PASS.
- Step 9: Run all workspace tests
Run: cargo test --workspace -p ailang-core workspace
Expected: green, including the inverted pin tests.
- Step 10: Create positive on-disk cross-module fixture pair
Create examples/mq1_xmod_constraint_class.ail.json (consumer module
with a qualified Constraint.class referencing the dep module's
class):
{
"schema": "ailang/v0",
"name": "mq1_xmod_constraint_class",
"imports": ["mq1_xmod_constraint_class_dep"],
"defs": [
{ "kind": "fn",
"name": "useShow",
"type": {
"k": "forall",
"vars": ["a"],
"constraints": [
{ "class": "mq1_xmod_constraint_class_dep.Show",
"type": { "k": "var", "name": "a" } }
],
"body": {
"k": "fn",
"params": [{ "k": "var", "name": "a" }],
"param_modes": ["borrow"],
"ret": { "k": "con", "name": "Unit" },
"effects": []
}
},
"params": ["x"],
"body": { "t": "var", "name": "x" }
}
]
}
Create examples/mq1_xmod_constraint_class_dep.ail.json:
{
"schema": "ailang/v0",
"name": "mq1_xmod_constraint_class_dep",
"imports": [],
"defs": [
{ "kind": "class",
"name": "Show",
"param": "a",
"methods": [
{ "name": "show",
"type": {
"k": "fn",
"params": [{ "k": "var", "name": "a" }],
"param_modes": ["borrow"],
"ret": { "k": "con", "name": "Str" },
"effects": []
}
}
]
}
]
}
(The exact schema-field names must match what the existing fixtures
under examples/ use. Implementer copies the structure from
examples/test_22b2_xmod_classmod.ail.json and adapts the consumer
side to use the qualified class ref.)
- Step 11: Add the on-disk fixture pin test
In crates/ailang-core/src/workspace.rs test module, add:
/// mq.1.3: on-disk fixture pin — a workspace where the consumer's
/// `Constraint.class` references a class in an imported module via
/// the qualified form loads cleanly. Symmetric to ct.1's positive
/// cross-module type-ref fixture.
#[test]
fn mq1_xmod_constraint_class_fixture_loads() {
let ws = load_workspace_from_paths(&[
Path::new("examples/mq1_xmod_constraint_class.ail.json"),
Path::new("examples/mq1_xmod_constraint_class_dep.ail.json"),
])
.expect("workspace must load with qualified Constraint.class");
assert!(ws.modules.contains_key("mq1_xmod_constraint_class"));
assert!(ws.modules.contains_key("mq1_xmod_constraint_class_dep"));
}
(The exact loader entry point may differ; the implementer matches the pattern used by other on-disk fixture pin tests in the same file.)
- Step 12: Run the new on-disk fixture test
Run: cargo test --workspace -p ailang-core mq1_xmod_constraint_class_fixture_loads
Expected: PASS.
Task 4: Re-key build_registry to qualified class names (Pass-1 + Pass-2 + lookup sites)
Files:
-
Modify:
crates/ailang-core/src/workspace.rs:521-545(Pass-1:class_def_module+class_by_namere-key) -
Modify:
crates/ailang-core/src/workspace.rs:629-755(Pass-2:entrieskey + 4 consumer sites) -
Step 1: Introduce the qualifier helper at module scope
Insert near the top of workspace.rs (before build_registry):
/// mq.1: take a class-ref field value (`InstanceDef.class`,
/// `SuperclassRef.class`, `Constraint.class`) and a defining context,
/// and produce the qualified workspace key. Bare ⇒ prepend the
/// caller_module; qualified ⇒ as-is. Symmetric to ct.1's
/// `normalize_type_for_registry` for `Type::Con`.
fn qualify_class_ref(class_ref: &str, caller_module: &str) -> String {
if class_ref.contains('.') {
class_ref.to_string()
} else {
format!("{caller_module}.{class_ref}")
}
}
- Step 2: Re-key Pass-1
class_def_moduleandclass_by_name
In build_registry at workspace.rs:521-545, replace:
let mut class_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 {
match def {
Def::Class(c) => {
class_def_module.insert(c.name.clone(), mod_name.clone());
class_by_name.insert(c.name.clone(), c);
}
// ...
}
}
}
with:
let mut class_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 {
match def {
Def::Class(c) => {
let qualified = format!("{mod_name}.{}", c.name);
class_def_module.insert(qualified.clone(), mod_name.clone());
class_by_name.insert(qualified, c);
}
// ...
}
}
}
Keys are now qualified; values unchanged.
- Step 3: Update the Pass-2 coherence lookup
At workspace.rs:645-648:
let class_mod = class_def_module
.get(&inst.class)
.cloned()
.unwrap_or_else(|| "<unknown-class>".into());
becomes:
let inst_class_key = qualify_class_ref(&inst.class, mod_name);
let class_mod = class_def_module
.get(&inst_class_key)
.cloned()
.unwrap_or_else(|| "<unknown-class>".into());
- Step 4: Update the Pass-2 method-completeness lookup
At workspace.rs:695:
if let Some(class_def) = class_by_name.get(&inst.class) {
becomes:
if let Some(class_def) = class_by_name.get(&inst_class_key) {
(Uses the inst_class_key already computed in Step 3.)
- Step 5: Update the Pass-2
entriesmap key
At workspace.rs:678:
let key = (inst.class.clone(), type_hash);
becomes:
let key = (inst_class_key.clone(), type_hash);
The DuplicateInstance diagnostic at lines 680-686 receives the
qualified class name via inst.class (still the surface schema value)
or via inst_class_key; the diagnostic surface uses inst.class (the
on-disk shape) for the human-readable message. Implementer chooses:
the simpler option keeps inst.class in the diagnostic and uses
inst_class_key only for the registry key. No diagnostic-wording
change required.
- Step 6: Update the superclass-instance check
At workspace.rs:744-754, find the existing sc.class-keyed
superclass-instance lookup. Replace the key construction with the
qualified form:
let sc_class_key = qualify_class_ref(&sc.class, class_mod_of_inst);
let sc_key = (sc_class_key, type_hash.clone());
(The class_mod_of_inst variable name is illustrative; implementer
identifies the correct "owner module" for the superclass ref —
typically the class_def_module value for the parent class, since
SuperclassRef is inside a ClassDef and lives in that class's
module.)
- Step 7: Update
Origin::Class.class_nameto qualified
At workspace.rs:561 and 582 (the two construction sites for
Origin::Class { class_name, module } inside the
MethodNameCollision pre-pass), wrap c.name.clone() in the qualifier:
let origin = Origin::Class {
class_name: format!("{mod_name}.{}", c.name),
module: mod_name.clone(),
};
Both sites. The Display::format impl at workspace.rs:567 stays
unchanged; output becomes "class M.Show (in M)" (informative even if
redundant).
- Step 8: Run the targeted
build_registrytests
Run: cargo test --workspace -p ailang-core registry
Expected: green. Existing registry tests assert outcomes by class-name match; qualified strings still match symmetrically.
- Step 9: Run all workspace tests
Run: cargo test --workspace -p ailang-core
Expected: green. Pin tests that string-match on Origin::Class.class_name
will fail here — those are Task 6's scope.
Task 5: Qualify ClassMethodEntry.class_name + class_superclasses in ailang-check
Files:
-
Modify:
crates/ailang-check/src/lib.rs:1102-1123(ClassMethodEntry doc) -
Modify:
crates/ailang-check/src/lib.rs:1195-1212(build_module_globals Class arm) -
Modify:
crates/ailang-check/src/lib.rs:1303-1312(class_superclasses build) -
Step 1: Update
ClassMethodEntry.class_namedoc-comment
In crates/ailang-check/src/lib.rs:1102-1123, find the doc-comment on
the class_name field. Append:
/// (mq.1: carries the qualified form `<defining_module>.<Class>`;
/// flows downstream into `ResidualConstraint.class` and
/// `MonoTarget::ClassMethod.class` unchanged.)
- Step 2: Qualify the insert site at lib.rs:1205
In build_module_globals at crates/ailang-check/src/lib.rs:1195-1212,
locate the Def::Class arm that inserts into class_methods. Find the
construction site of ClassMethodEntry { class_name: cd.name.clone(), ... }
(approximately line 1205). Replace with:
let qualified_class = format!("{mname}.{}", cd.name);
class_methods.insert(
method.name.clone(),
ClassMethodEntry {
class_name: qualified_class.clone(),
// ...other fields unchanged
},
);
(The variable mname is the iteration variable for the per-module
loop; implementer confirms the exact name in scope.)
- Step 3: Qualify
class_superclasseskey and value
In crates/ailang-check/src/lib.rs:1303-1312, find the
class_superclasses build loop. The current shape is approximately:
for (_mod_name, m) in &ws.modules {
for def in &m.defs {
if let Def::Class(cd) = def {
if let Some(sc) = &cd.superclass {
class_superclasses.insert(cd.name.clone(), sc.class.clone());
}
}
}
}
Replace with:
for (mod_name, m) in &ws.modules {
for def in &m.defs {
if let Def::Class(cd) = def {
if let Some(sc) = &cd.superclass {
let class_qualified = format!("{mod_name}.{}", cd.name);
let sc_qualified = if sc.class.contains('.') {
sc.class.clone()
} else {
format!("{mod_name}.{}", sc.class)
};
class_superclasses.insert(class_qualified, sc_qualified);
}
}
}
}
(If the loop variable is _mod_name today, rename to mod_name. If
the map type signature carries explicit String keys/values, leave it
as-is; only the value content changes.)
- Step 4: Run targeted ailang-check tests
Run: cargo test --workspace -p ailang-check
Expected: green at the lib level; pin tests in crates/ail/tests/
asserting against bare class strings are Task 6's scope.
- Step 5: Spot-check synth Var-arm + mono targets compile
Run: cargo build --workspace
Expected: green. The Var-arm at lib.rs:1968 reads cm.class_name
(now qualified); residual emission at line 1991 carries the qualified
string. Mono's class_index.get(class) lookups (in mono.rs:137,
634, 1185) read qualified keys and match qualified MonoTarget.class
values. No code change needed in these consumers.
Task 6: Update test-assertion strings affected by qualification migration
Files:
-
Modify:
crates/ailang-core/src/workspace.rs:1937-1996(MethodNameCollisionpin tests) -
Modify:
crates/ail/tests/typeclass_22b2.rs:42(class_method_class assertion) -
Modify:
crates/ail/tests/typeclass_22b3.rs:151, 217, 232, 295, 301, 341, 353(class-string assertions) -
Step 1: Update
MethodNameCollisionpin tests at workspace.rs:1937-1996
Open crates/ailang-core/src/workspace.rs and locate the two
MethodNameCollision pin tests (class_class_method_name_collision_fires,
class_fn_method_name_collision_fires — exact names per recon).
For each pin test, find the assertion lines that match against the old bare-class format. The shapes are typically:
assert!(first_origin.starts_with("class A"), ...);
Replace with the post-mq.1 qualified form (the actual module name in the fixture is what determines the prefix):
// mq.1: class_name now qualified — the Display format emits
// "class <module>.<Class> (in <module>)"
assert!(first_origin.starts_with("class m.A") || first_origin.contains(".A"),
"got first_origin: {first_origin}");
(The exact module name m is whatever the test fixture declares.
Implementer reads each test's Module construction to derive the right
prefix.)
- Step 2: Update
typeclass_22b2.rs:42
In crates/ail/tests/typeclass_22b2.rs:42, find:
assert_eq!(
mod_globals.class_method_class("show"),
Some("Show"),
);
Determine the module name in the fixture (the test loads a fixture
named test_22b2_class_method_lookup per recon), and replace with:
// mq.1: class_name now qualified
assert_eq!(
mod_globals.class_method_class("show"),
Some("test_22b2_class_method_lookup.Show"),
);
- Step 3: Update
typeclass_22b3.rsclass-string assertions
In crates/ail/tests/typeclass_22b3.rs, locate the assertion sites
identified by recon (lines 151, 217, 232, 295, 301, 341, 353).
Typical patterns:
assert_eq!(class, "Show", ...);
assert_eq!(class, "Foo", ...);
assert_eq!(class, "Greet", ...);
Replace each with the qualified form. The module prefix depends on the test's fixture/Module construction:
- Inline
Module { name: "m", ... }tests → prefix"m.". - On-disk-fixture tests → the fixture's module name (typically the
filename without
.ail.json).
Examples:
// mq.1: qualified class names
assert_eq!(class, "test_22b2_instance_present.Show", ...);
assert_eq!(class, "m.Foo", ...);
assert_eq!(class, "m.Greet", ...);
Implementer reads each test's fixture context to pick the right prefix. Each assertion update is one line.
- Step 4: Run the workspace assertion-touched tests
Run:
cargo test --workspace -p ailang-core workspace::tests::class_class_method_name_collision_fires \
workspace::tests::class_fn_method_name_collision_fires
Expected: PASS.
- Step 5: Run the typeclass test files
Run:
cargo test --workspace --test typeclass_22b2 --test typeclass_22b3
Expected: PASS.
Task 7: Integration verification — full workspace + bench scripts + roundtrip
Files: none modified; verification only.
- Step 1: Full
cargo test --workspace
Run: cargo test --workspace
Expected: green. All 16 binary-test suites pass (~600 tests).
- Step 2: Verify zero diff on
examples/prelude.ail.json
Run: git diff -- examples/prelude.ail.json
Expected: no output (spec assumption 17: intra-prelude class refs all stay bare, prelude unchanged).
- Step 3: Verify zero diff on the 14
test_22b*fixtures
Run: git diff -- examples/test_22b*.ail.json
Expected: no output (recon-confirmed: all existing class refs in those fixtures are intra-module; canonical-form rule keeps them bare).
- Step 4: Bench script
bench/check.py
Run: python3 bench/check.py
Expected: exit 0. (If exit non-zero, audit-ratify per audit-skill convention — note in the journal whether the regression is in-scope or spurious.)
- Step 5: Bench script
bench/compile_check.py
Run: python3 bench/compile_check.py
Expected: exit 0 OR audit-ratified.
- Step 6: Bench script
bench/cross_lang.py
Run: python3 bench/cross_lang.py
Expected: exit 0 OR audit-ratified.
- Step 7: Roundtrip the new on-disk fixture
Run:
cargo run -p ail -- check examples/mq1_xmod_constraint_class.ail.json
Expected: exit 0, no diagnostics.
- Step 8: Confirm the per-iter journal placeholder is staged
The implementer leaves docs/journals/2026-05-13-iter-mq.1.md (or the
date the iter actually finishes) in the working tree with a draft
entry covering: tasks ran, deviations from plan (if any),
spec-cost-claim discrepancy (the "14 fixtures migrated" claim was over-
stated; recon-confirmed zero existing-fixture diff), test counts, bench
outcomes. Append a pointer line to docs/journals/INDEX.md. The Boss
commits.