1a5f8289b7
Iter mq.1 plan: move InstanceDef.class / Constraint.class / SuperclassRef.class from bare-only to canonical form per ct.1's existing rule, extend validate_canonical_type_names with three new field walks, narrow check_class_name_fields to ClassDef.name-only, qualify build_registry's class_def_module + class_by_name + entries + four lookup sites, qualify ClassMethodEntry.class_name + class_superclasses, update affected test-assertion strings. Pre-flight recon flagged that the spec's '14 fixtures migrated' cost claim is over-stated: all existing fixture class refs are intra-module under the canonical-form rule. Plan does not add migration work that does not need doing; Task 7 Step 3 verifies zero existing-fixture diff.
1201 lines
41 KiB
Markdown
1201 lines
41 KiB
Markdown
# Iter mq.1 — Canonical-form extension for class-ref fields — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/2026-05-13-module-qualified-class-names.md`
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` to
|
|
> 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 after `BareCrossModuleTypeRef` / `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):
|
|
|
|
```rust
|
|
/// 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:
|
|
|
|
```rust
|
|
/// 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:
|
|
|
|
```rust
|
|
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.name` doc-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:
|
|
|
|
```rust
|
|
/// 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.class` doc-comment**
|
|
|
|
In `crates/ailang-core/src/ast.rs:277-284`, replace the
|
|
`/// Superclass name.` line above `pub class: String` with:
|
|
|
|
```rust
|
|
/// 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.class` doc-comment**
|
|
|
|
In `crates/ailang-core/src/ast.rs:312-317`, replace the
|
|
`/// Class being instantiated.` line above `pub class: String` with:
|
|
|
|
```rust
|
|
/// 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.class` doc-comment**
|
|
|
|
In `crates/ailang-core/src/ast.rs:343-349`, replace the
|
|
`/// Class name.` line above `pub class: String` with:
|
|
|
|
```rust
|
|
/// 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_fields` narrows 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.rs` `mod tests` (3 RED-first pin tests for `BareCrossModuleClassRef`, 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`:
|
|
|
|
```rust
|
|
/// 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:
|
|
|
|
```rust
|
|
/// 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:
|
|
|
|
```rust
|
|
/// 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:
|
|
|
|
```rust
|
|
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_fields` to `ClassDef.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:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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):
|
|
|
|
```json
|
|
{
|
|
"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`:
|
|
|
|
```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:
|
|
|
|
```rust
|
|
/// 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_name` re-key)
|
|
- Modify: `crates/ailang-core/src/workspace.rs:629-755` (Pass-2: `entries` key + 4 consumer sites)
|
|
|
|
- [ ] **Step 1: Introduce the qualifier helper at module scope**
|
|
|
|
Insert near the top of `workspace.rs` (before `build_registry`):
|
|
|
|
```rust
|
|
/// 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_module` and `class_by_name`**
|
|
|
|
In `build_registry` at `workspace.rs:521-545`, replace:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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`:
|
|
|
|
```rust
|
|
let class_mod = class_def_module
|
|
.get(&inst.class)
|
|
.cloned()
|
|
.unwrap_or_else(|| "<unknown-class>".into());
|
|
```
|
|
|
|
becomes:
|
|
|
|
```rust
|
|
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`:
|
|
|
|
```rust
|
|
if let Some(class_def) = class_by_name.get(&inst.class) {
|
|
```
|
|
|
|
becomes:
|
|
|
|
```rust
|
|
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 `entries` map key**
|
|
|
|
At `workspace.rs:678`:
|
|
|
|
```rust
|
|
let key = (inst.class.clone(), type_hash);
|
|
```
|
|
|
|
becomes:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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_name` to 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:
|
|
|
|
```rust
|
|
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_registry` tests**
|
|
|
|
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_name` doc-comment**
|
|
|
|
In `crates/ailang-check/src/lib.rs:1102-1123`, find the doc-comment on
|
|
the `class_name` field. Append:
|
|
|
|
```rust
|
|
/// (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:
|
|
|
|
```rust
|
|
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_superclasses` key and value**
|
|
|
|
In `crates/ailang-check/src/lib.rs:1303-1312`, find the
|
|
`class_superclasses` build loop. The current shape is approximately:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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` (`MethodNameCollision` pin 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 `MethodNameCollision` pin 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:
|
|
|
|
```rust
|
|
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):
|
|
|
|
```rust
|
|
// 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:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
// 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.rs` class-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:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
// 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.
|