plan: ct.4 milestone close
This commit is contained in:
@@ -0,0 +1,930 @@
|
|||||||
|
# Iteration ct.4: Milestone Close — Implementation Plan
|
||||||
|
|
||||||
|
> **Parent spec:** `docs/specs/2026-05-10-canonical-type-names.md`
|
||||||
|
>
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
||||||
|
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||||
|
|
||||||
|
**Goal:** Land the milestone-closing consolidation: amend DESIGN.md
|
||||||
|
with the canonical-form rule, pin hashes for the two migrated
|
||||||
|
cross-module fixtures, add qualifier-trim-on-print to the prose
|
||||||
|
printer with a snapshot test, and recreate
|
||||||
|
`examples/compare_primitives_smoke.ail.json` as the end-to-end
|
||||||
|
demonstration that closes the iter-23.3-Task-4 bug.
|
||||||
|
|
||||||
|
**Architecture:** Four tasks across DESIGN, hash regression, prose
|
||||||
|
printer, and end-to-end fixture. Task 1 is design writing. Task 2
|
||||||
|
adds new hash-pin tests (the existing pins on `sum`/`list` are
|
||||||
|
intra-module and unaffected; the migrated `ordering_match` /
|
||||||
|
`test_22b1_dup_a` get new pins to lock the post-migration state).
|
||||||
|
Task 3 threads `module_name` context through the prose printer so
|
||||||
|
intra-module qualified Type::Cons get trimmed on print, plus a
|
||||||
|
snapshot for `ordering_match`. Task 4 authors the
|
||||||
|
`compare_primitives_smoke` fixture (9 Ord.compare invocations
|
||||||
|
across Int/Bool/Str, expected stdout `1\n2\n3\n1\n2\n3\n1\n2\n3\n`)
|
||||||
|
and adds an E2E test plus an IR-shape integration test asserting
|
||||||
|
the compare__T mono symbols are emitted.
|
||||||
|
|
||||||
|
**Tech Stack:** `docs/DESIGN.md`, `crates/ailang-core/src/hash.rs`,
|
||||||
|
`crates/ailang-prose/src/lib.rs`, `crates/ailang-prose/tests/snapshot.rs`,
|
||||||
|
`examples/*.ail.json` + `.prose.txt`, `crates/ail/tests/e2e.rs`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files this plan creates or modifies
|
||||||
|
|
||||||
|
- Modify: `docs/DESIGN.md` — amend Decision 2 (around line 1067)
|
||||||
|
with the canonical-form rule for `Type::Con` names; review the
|
||||||
|
surrounding hash-stability promises at lines 1121, 1136-1139 to
|
||||||
|
make sure they explicitly cite the migrated fixtures as the
|
||||||
|
named exception.
|
||||||
|
- Modify: `crates/ailang-core/src/hash.rs` — append three new pin
|
||||||
|
tests near the existing `iter22b1_schema_extension_preserves_pre_22b_hashes`
|
||||||
|
block: one pinning the new canonical hashes of the two migrated
|
||||||
|
fixtures, one asserting `sum.ail.json` and `list.ail.json` (both
|
||||||
|
intra-module-only) still hash bit-identically.
|
||||||
|
- Modify: `crates/ailang-prose/src/lib.rs` — thread an
|
||||||
|
`owning_module: &str` context through `write_module` → `write_def`
|
||||||
|
→ ... → `write_type`; in `write_type`'s `Type::Con` arm, trim a
|
||||||
|
qualifier whose owner matches `owning_module`.
|
||||||
|
- Modify: `crates/ailang-prose/tests/snapshot.rs` — add one test
|
||||||
|
`snapshot_ordering_match` consuming the new
|
||||||
|
`examples/ordering_match.prose.txt` snapshot.
|
||||||
|
- Create: `examples/ordering_match.prose.txt` — committed prose
|
||||||
|
snapshot for the migrated cross-module fixture.
|
||||||
|
- Create: `examples/compare_primitives_smoke.ail.json` — the
|
||||||
|
iter-23.3 demonstration fixture.
|
||||||
|
- Modify: `crates/ail/tests/e2e.rs` — append two tests:
|
||||||
|
`compare_primitives_smoke_prints_1_2_3_thrice` (E2E) and
|
||||||
|
`compare_primitives_smoke_emits_compare_mono_symbols` (IR shape).
|
||||||
|
|
||||||
|
No files are deleted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: DESIGN.md Decision 2 amendment
|
||||||
|
|
||||||
|
Decision 2 (content-addressed defs) in DESIGN.md is the spec of
|
||||||
|
the `.ail.json` canonical form. It currently does not pin down
|
||||||
|
how `Type::Con` names are scoped — that policy lived in
|
||||||
|
JOURNAL-level prose until ct.1 made it normative. This task
|
||||||
|
writes the rule into DESIGN.md so future iters check against it
|
||||||
|
during architect drift review.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/DESIGN.md` near line 1067 (Decision 2 / canonical
|
||||||
|
form) — add a new subsection or paragraph naming the rule.
|
||||||
|
- Modify: `docs/DESIGN.md` line 1136-1139 — update the
|
||||||
|
`iter19b_*` regression-pin citation to also name the new
|
||||||
|
ct.1-era pins for migrated fixtures.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Read the Decision 2 surrounding context**
|
||||||
|
|
||||||
|
Read `docs/DESIGN.md` lines 1050-1145 (approximately). Identify
|
||||||
|
the natural insertion point for the canonical-form rule — likely
|
||||||
|
adjacent to the existing "JSON canonical hash for every existing
|
||||||
|
fixture stays bit-identical" paragraph at 1067-1070.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Insert the canonical-form rule**
|
||||||
|
|
||||||
|
Edit `docs/DESIGN.md`. After the paragraph at lines 1067-1070
|
||||||
|
("JSON canonical hash for every existing fixture stays bit-
|
||||||
|
identical..."), insert a new paragraph block:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**Type::Con name scoping (canonical form, since ct.1).** Within a
|
||||||
|
`.ail.json`, a `Type::Con.name` is interpreted relative to the
|
||||||
|
file's top-level `"name"` field (the owning module). Bare names
|
||||||
|
(no `.`) refer to a TypeDef in the owning module's own `defs`.
|
||||||
|
Cross-module references MUST be qualified `<owning_module>.<TypeName>`
|
||||||
|
where `<owning_module>` is a known module in the workspace.
|
||||||
|
Primitives (`Int`, `Bool`, `Str`, `Unit`, `Float`) are bare and
|
||||||
|
have no module qualifier. Bare cross-module references are a
|
||||||
|
schema violation (`WorkspaceLoadError::BareCrossModuleTypeRef`);
|
||||||
|
qualified references whose owner is unknown are also a violation
|
||||||
|
(`WorkspaceLoadError::BadCrossModuleTypeRef`). The same rule
|
||||||
|
applies to `Term::Ctor.type_name`.
|
||||||
|
|
||||||
|
Class names (`ClassDef.name`, `InstanceDef.class`,
|
||||||
|
`SuperclassRef.class`, `Constraint.class`) are NOT module-scoped
|
||||||
|
under this rule; they remain workspace-flat with
|
||||||
|
`MethodNameCollision` enforced at load. Class-name scoping is a
|
||||||
|
future milestone with its own DESIGN amendment.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update the regression-pin citation at 1136-1139**
|
||||||
|
|
||||||
|
Edit `docs/DESIGN.md`. Find the paragraph at lines 1136-1139 that
|
||||||
|
ends with "regression-pinned by `iter19b_empty_suppress_preserves_pre_19b_hashes`
|
||||||
|
and `iter19b_schema_extension_preserves_pre_19b_hashes`."
|
||||||
|
|
||||||
|
Append at the end of that paragraph:
|
||||||
|
|
||||||
|
```
|
||||||
|
The canonical-form tightening in ct.1 shifted the hashes of two
|
||||||
|
cross-module fixtures (`ordering_match.ail.json` and
|
||||||
|
`test_22b1_dup_a.ail.json`); all intra-module fixtures, including
|
||||||
|
the regression-pinned `sum.ail.json` and `list.ail.json`, remain
|
||||||
|
bit-identical. The new pins are
|
||||||
|
`ct4_migrated_fixtures_have_canonical_form_hashes` (locks the
|
||||||
|
post-migration hashes) and
|
||||||
|
`ct4_unmigrated_fixtures_remain_bit_identical` (re-asserts the
|
||||||
|
existing 13a/19b/22b.1 hashes still hold).
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify DESIGN.md still parses cleanly**
|
||||||
|
|
||||||
|
Run: `cargo doc --workspace --no-deps 2>&1 | grep -i 'DESIGN\.md\|warning' | head`
|
||||||
|
|
||||||
|
Expected: no new errors (DESIGN.md is rustdoc-rendered in places;
|
||||||
|
the additions are plain markdown with no special syntax).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/DESIGN.md
|
||||||
|
git commit -m "iter ct.4.1: DESIGN.md Decision 2 — canonical Type::Con name scoping rule"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Hash pins for migrated fixtures
|
||||||
|
|
||||||
|
The existing pin tests in `crates/ailang-core/src/hash.rs` use
|
||||||
|
`sum.ail.json` and `list.ail.json` — both intra-module, neither
|
||||||
|
touched by the ct.1 migration. Their pinned hashes remain valid
|
||||||
|
and don't need re-baselining. This task ADDS new pins that lock
|
||||||
|
in the canonical-form hashes of the two migrated fixtures, plus
|
||||||
|
re-asserts the unmigrated pins still hold.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `crates/ailang-core/src/hash.rs` — append two new tests
|
||||||
|
at the end of `mod tests`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Capture the current hashes of the migrated fixtures**
|
||||||
|
|
||||||
|
Run the following one-liner to print the canonical hash of each
|
||||||
|
migrated fixture's relevant def. From a Rust REPL is overkill;
|
||||||
|
write a single throwaway test in `crates/ailang-core/src/hash.rs`
|
||||||
|
that asserts known-wrong hashes (so it fails and prints the
|
||||||
|
actual hash):
|
||||||
|
|
||||||
|
Add inside `mod tests`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
#[ignore] // helper: run manually with --ignored
|
||||||
|
fn print_migrated_fixture_hashes() {
|
||||||
|
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let examples = manifest_dir.join("../../examples");
|
||||||
|
|
||||||
|
let ord_src = std::fs::read(examples.join("ordering_match.ail.json")).unwrap();
|
||||||
|
let ord_mod: crate::ast::Module = serde_json::from_slice(&ord_src).unwrap();
|
||||||
|
let main_def = ord_mod.defs.iter().find(|d| d.name() == "main").unwrap();
|
||||||
|
eprintln!("ordering_match::main: {}", def_hash(main_def));
|
||||||
|
|
||||||
|
let dup_a_src = std::fs::read(examples.join("test_22b1_dup_a.ail.json")).unwrap();
|
||||||
|
let dup_a_mod: crate::ast::Module = serde_json::from_slice(&dup_a_src).unwrap();
|
||||||
|
for d in &dup_a_mod.defs {
|
||||||
|
eprintln!("test_22b1_dup_a::{}: {}", d.name(), def_hash(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("captured hashes — see stderr");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `cargo test --workspace -p ailang-core print_migrated_fixture_hashes --ignored -- --nocapture 2>&1 | grep '::'`
|
||||||
|
|
||||||
|
Expected: prints hash lines for `ordering_match::main` and each
|
||||||
|
`test_22b1_dup_a::<def_name>`. Record the hashes for use in Step 2.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace the helper with the real pin test**
|
||||||
|
|
||||||
|
Edit `crates/ailang-core/src/hash.rs` — replace the throwaway
|
||||||
|
helper test with the real pin:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Iter ct.4 (canonical-type-names milestone close): pin the
|
||||||
|
/// canonical-form hashes of the two cross-module fixtures
|
||||||
|
/// migrated by `ail migrate-canonical-types` in ct.1. These
|
||||||
|
/// hashes are the post-migration state; pre-migration values
|
||||||
|
/// (when both fixtures carried bare cross-module Type::Con)
|
||||||
|
/// are no longer reproducible because the ct.1 validator
|
||||||
|
/// rejects that shape upstream.
|
||||||
|
#[test]
|
||||||
|
fn ct4_migrated_fixtures_have_canonical_form_hashes() {
|
||||||
|
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let examples = manifest_dir.join("../../examples");
|
||||||
|
|
||||||
|
let ord_src = std::fs::read(examples.join("ordering_match.ail.json"))
|
||||||
|
.expect("examples/ordering_match.ail.json present");
|
||||||
|
let ord_mod: crate::ast::Module = serde_json::from_slice(&ord_src).unwrap();
|
||||||
|
let main_def = ord_mod.defs.iter().find(|d| d.name() == "main").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
def_hash(main_def),
|
||||||
|
// Replace this string with the actual hash printed in Step 1.
|
||||||
|
"REPLACE_WITH_CAPTURED_HASH_FROM_STEP_1",
|
||||||
|
"ordering_match::main canonical hash must match captured post-migration value"
|
||||||
|
);
|
||||||
|
|
||||||
|
let dup_a_src = std::fs::read(examples.join("test_22b1_dup_a.ail.json"))
|
||||||
|
.expect("examples/test_22b1_dup_a.ail.json present");
|
||||||
|
let dup_a_mod: crate::ast::Module = serde_json::from_slice(&dup_a_src).unwrap();
|
||||||
|
// Pin the first def's hash. If test_22b1_dup_a has multiple defs,
|
||||||
|
// pin only the type def that carries the migrated qualifier.
|
||||||
|
let first_def = dup_a_mod.defs.first().expect("at least one def");
|
||||||
|
assert_eq!(
|
||||||
|
def_hash(first_def),
|
||||||
|
"REPLACE_WITH_SECOND_CAPTURED_HASH_FROM_STEP_1",
|
||||||
|
"test_22b1_dup_a::{} canonical hash must match captured post-migration value",
|
||||||
|
first_def.name(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll need to substitute the actual hash strings from Step 1's
|
||||||
|
captured output and remove the placeholder strings. If
|
||||||
|
`test_22b1_dup_a` has multiple defs of relevance, pin each (one
|
||||||
|
`assert_eq!` per def).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add a re-assertion pin for unmigrated fixtures**
|
||||||
|
|
||||||
|
Edit `crates/ailang-core/src/hash.rs` — append a second new test:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Iter ct.4: re-assert that the canonical-form tightening
|
||||||
|
/// did NOT change hashes of intra-module-only fixtures. The
|
||||||
|
/// existing iter-13a / iter-19b / iter-22b.1 pin tests already
|
||||||
|
/// assert these; this one names the canonical-type-names
|
||||||
|
/// milestone explicitly so future archaeology finds the
|
||||||
|
/// connection.
|
||||||
|
#[test]
|
||||||
|
fn ct4_unmigrated_fixtures_remain_bit_identical() {
|
||||||
|
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let examples = manifest_dir.join("../../examples");
|
||||||
|
|
||||||
|
let sum_src = std::fs::read(examples.join("sum.ail.json")).unwrap();
|
||||||
|
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
|
||||||
|
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
||||||
|
assert_eq!(def_hash(sum_def), "db33f57cb329935e",
|
||||||
|
"sum.sum hash drifted across canonical-form tightening — unexpected");
|
||||||
|
|
||||||
|
let list_src = std::fs::read(examples.join("list.ail.json")).unwrap();
|
||||||
|
let list_mod: crate::ast::Module = serde_json::from_slice(&list_src).unwrap();
|
||||||
|
let int_list_def = list_mod.defs.iter().find(|d| d.name() == "IntList").unwrap();
|
||||||
|
assert_eq!(def_hash(int_list_def), "b082192bd0c99202",
|
||||||
|
"list.IntList hash drifted across canonical-form tightening — unexpected");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the pin tests**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace -p ailang-core ct4_migrated_fixtures_have_canonical_form_hashes ct4_unmigrated_fixtures_remain_bit_identical`
|
||||||
|
|
||||||
|
Expected: BOTH PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the full test suite**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace`
|
||||||
|
|
||||||
|
Expected: all pass (test count grows by 2).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add crates/ailang-core/src/hash.rs
|
||||||
|
git commit -m "iter ct.4.2: pin canonical-form hashes for migrated fixtures + unmigrated sanity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Prose qualifier-trim-on-print + snapshot
|
||||||
|
|
||||||
|
The prose printer at `crates/ailang-prose/src/lib.rs:462` writes
|
||||||
|
`Type::Con.name` verbatim. When the canonical `.ail.json` already
|
||||||
|
has bare names for intra-module Type::Cons (the canonical
|
||||||
|
convention), this is fine. But the validator does not reject
|
||||||
|
qualified intra-module refs (it only rejects bare cross-module
|
||||||
|
and bad-qualified); a hand-authored fixture could legally have
|
||||||
|
`prelude.Ordering` inside prelude's own file. The prose form
|
||||||
|
should print this as bare `Ordering` so the prose surface stays
|
||||||
|
LLM-author-friendly (qualifier omitted because it follows from
|
||||||
|
the file's module name).
|
||||||
|
|
||||||
|
This task threads `owning_module` context through the prose
|
||||||
|
printer and trims at `write_type`'s `Type::Con` arm. Plus a
|
||||||
|
snapshot test that pins the print behaviour for the migrated
|
||||||
|
cross-module fixture (`ordering_match.ail.json`, which uses
|
||||||
|
`prelude.Ordering` cross-module qualifier — the trim does NOT
|
||||||
|
fire here because the qualifier's owner differs from the file's
|
||||||
|
module).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `crates/ailang-prose/src/lib.rs` — thread `owning_module`
|
||||||
|
through every `write_*` fn signature from `write_module` down to
|
||||||
|
`write_type`; trim in `write_type`'s `Type::Con` arm.
|
||||||
|
- Create: `examples/ordering_match.prose.txt` — committed
|
||||||
|
snapshot.
|
||||||
|
- Modify: `crates/ailang-prose/tests/snapshot.rs` — register one
|
||||||
|
new snapshot test.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the RED test for trim behaviour**
|
||||||
|
|
||||||
|
Append to `crates/ailang-prose/src/lib.rs` `mod tests` (or wherever
|
||||||
|
the existing unit tests live — search for `mod tests` near the
|
||||||
|
bottom of the file):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// ct.4 Task 3: when a `Type::Con.name` is qualified with the
|
||||||
|
/// owning module's own name (e.g. `prelude.Ordering` inside
|
||||||
|
/// `prelude`'s own file), the prose printer must trim the
|
||||||
|
/// qualifier and emit bare `Ordering`. Cross-module qualified
|
||||||
|
/// refs (`prelude.Ordering` inside any non-prelude file)
|
||||||
|
/// round-trip verbatim.
|
||||||
|
#[test]
|
||||||
|
fn type_con_qualifier_trimmed_when_owner_matches_module() {
|
||||||
|
use ailang_core::ast::{Module, Def, FnDef, Type, Term, Literal, ParamMode};
|
||||||
|
|
||||||
|
let m = Module {
|
||||||
|
schema: "ailang/v0".into(),
|
||||||
|
name: "prelude".into(),
|
||||||
|
imports: vec![],
|
||||||
|
defs: vec![Def::Fn(FnDef {
|
||||||
|
name: "noop".into(),
|
||||||
|
ty: Type::Fn {
|
||||||
|
params: vec![],
|
||||||
|
ret: Box::new(Type::Con {
|
||||||
|
name: "prelude.Ordering".into(),
|
||||||
|
args: vec![],
|
||||||
|
}),
|
||||||
|
effects: vec![],
|
||||||
|
param_modes: vec![],
|
||||||
|
ret_mode: ParamMode::Implicit,
|
||||||
|
},
|
||||||
|
params: vec![],
|
||||||
|
body: Term::Ctor {
|
||||||
|
type_name: "prelude.Ordering".into(),
|
||||||
|
ctor: "EQ".into(),
|
||||||
|
args: vec![],
|
||||||
|
},
|
||||||
|
doc: None,
|
||||||
|
suppress: vec![],
|
||||||
|
})],
|
||||||
|
};
|
||||||
|
|
||||||
|
let prose = module_to_prose(&m);
|
||||||
|
assert!(
|
||||||
|
prose.contains("-> Ordering"),
|
||||||
|
"expected bare `Ordering` in fn return type (owner == file's \
|
||||||
|
module); got prose:\n{}",
|
||||||
|
prose
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!prose.contains("prelude.Ordering"),
|
||||||
|
"expected NO `prelude.Ordering` after trim; got prose:\n{}",
|
||||||
|
prose
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ct.4 Task 3: a cross-module Type::Con (owner != current
|
||||||
|
/// file's module) must round-trip verbatim — no trim.
|
||||||
|
#[test]
|
||||||
|
fn type_con_qualifier_preserved_when_owner_differs() {
|
||||||
|
use ailang_core::ast::{Module, Def, FnDef, Type, Term, Literal, ParamMode, Import};
|
||||||
|
|
||||||
|
let m = Module {
|
||||||
|
schema: "ailang/v0".into(),
|
||||||
|
name: "user".into(),
|
||||||
|
imports: vec![Import { module: "prelude".into(), alias: None }],
|
||||||
|
defs: vec![Def::Fn(FnDef {
|
||||||
|
name: "lt".into(),
|
||||||
|
ty: Type::Fn {
|
||||||
|
params: vec![],
|
||||||
|
ret: Box::new(Type::Con {
|
||||||
|
name: "prelude.Ordering".into(),
|
||||||
|
args: vec![],
|
||||||
|
}),
|
||||||
|
effects: vec![],
|
||||||
|
param_modes: vec![],
|
||||||
|
ret_mode: ParamMode::Implicit,
|
||||||
|
},
|
||||||
|
params: vec![],
|
||||||
|
body: Term::Ctor {
|
||||||
|
type_name: "prelude.Ordering".into(),
|
||||||
|
ctor: "LT".into(),
|
||||||
|
args: vec![],
|
||||||
|
},
|
||||||
|
doc: None,
|
||||||
|
suppress: vec![],
|
||||||
|
})],
|
||||||
|
};
|
||||||
|
|
||||||
|
let prose = module_to_prose(&m);
|
||||||
|
assert!(
|
||||||
|
prose.contains("prelude.Ordering"),
|
||||||
|
"expected qualified `prelude.Ordering` preserved (owner != file's \
|
||||||
|
module); got prose:\n{}",
|
||||||
|
prose
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the RED tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace -p ailang-prose type_con_qualifier_trimmed type_con_qualifier_preserved`
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `type_con_qualifier_trimmed_when_owner_matches_module`: FAIL (current printer writes `prelude.Ordering` verbatim).
|
||||||
|
- `type_con_qualifier_preserved_when_owner_differs`: PASS (current printer already writes qualified verbatim).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Thread `owning_module` through the print signatures**
|
||||||
|
|
||||||
|
Edit `crates/ailang-prose/src/lib.rs`. Modify the following
|
||||||
|
signatures to add `owning_module: &str` as the last parameter
|
||||||
|
(after `level: usize` where present, before `out`-mutating return
|
||||||
|
of `()`):
|
||||||
|
|
||||||
|
- `write_def`
|
||||||
|
- `write_type_def`
|
||||||
|
- `write_fn_def`
|
||||||
|
- `write_const_def`
|
||||||
|
- `write_class_def`
|
||||||
|
- `write_instance_def`
|
||||||
|
- `write_type`
|
||||||
|
- `write_mode_type` (if it exists; check)
|
||||||
|
- any other `write_*` that consumes a `Type` or `Term`
|
||||||
|
|
||||||
|
For each call site within the same file, add the propagated
|
||||||
|
`m.name.as_str()` argument from the entry point (`module_to_prose`
|
||||||
|
passes `m.name.as_str()` to `write_module`'s helper, which then
|
||||||
|
threads it to every `write_def` call).
|
||||||
|
|
||||||
|
Refactor the entry point `module_to_prose`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn module_to_prose(m: &Module) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
write_module(&mut out, m);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
stays the same; the threading begins inside `write_module`.
|
||||||
|
|
||||||
|
`write_module`'s body — the `m.name.as_str()` is captured locally
|
||||||
|
(let `module_name = m.name.as_str();`) and passed to each
|
||||||
|
`write_def(&mut out, def, level, module_name)` call.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Apply the trim in `write_type`'s `Type::Con` arm**
|
||||||
|
|
||||||
|
Edit `crates/ailang-prose/src/lib.rs` around line 465. The `Type::Con`
|
||||||
|
arm currently reads:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Type::Con { name, args } => {
|
||||||
|
out.push_str(name);
|
||||||
|
// ... args writing
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Type::Con { name, args } => {
|
||||||
|
// Trim a qualifier whose owner matches the current
|
||||||
|
// file's module: `prelude.Ordering` printed from
|
||||||
|
// inside `prelude` becomes bare `Ordering`. Cross-
|
||||||
|
// module qualifiers (`prelude.Ordering` printed from
|
||||||
|
// inside any non-prelude module) survive verbatim.
|
||||||
|
let display_name = match name.split_once('.') {
|
||||||
|
Some((owner, suffix)) if owner == owning_module => suffix,
|
||||||
|
_ => name.as_str(),
|
||||||
|
};
|
||||||
|
out.push_str(display_name);
|
||||||
|
// ... args writing (preserved verbatim, recursing
|
||||||
|
// into write_type with the same owning_module)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Where the args recursion calls `write_type(out, a)`, update to
|
||||||
|
`write_type(out, a, owning_module)`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Apply the same trim in `Term::Ctor`'s type_name (in write_term, wherever it lives)**
|
||||||
|
|
||||||
|
The prose printer also writes `Term::Ctor.type_name` somewhere
|
||||||
|
in its term-lowering. Search for `Term::Ctor` in `crates/ailang-prose/src/lib.rs`
|
||||||
|
and find the analogous `out.push_str(type_name)` site. Apply the
|
||||||
|
same trim logic:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// in the Term::Ctor write arm — find the existing string-push site
|
||||||
|
let display_type = match type_name.split_once('.') {
|
||||||
|
Some((owner, suffix)) if owner == owning_module => suffix,
|
||||||
|
_ => type_name.as_str(),
|
||||||
|
};
|
||||||
|
out.push_str(display_type);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the RED tests now expecting GREEN**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace -p ailang-prose type_con_qualifier_trimmed type_con_qualifier_preserved`
|
||||||
|
|
||||||
|
Expected: BOTH PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Regenerate existing prose snapshots**
|
||||||
|
|
||||||
|
The trim changes how `prelude.Ordering` (and any other
|
||||||
|
qualifier-matching pattern) renders. Most existing prose
|
||||||
|
snapshots are intra-module-only fixtures with no qualified
|
||||||
|
Type::Cons — they should be byte-identical. Verify:
|
||||||
|
|
||||||
|
Run: `cargo test --workspace -p ailang-prose`
|
||||||
|
|
||||||
|
Expected: all existing snapshot tests still pass. If any fail
|
||||||
|
with a `.prose.txt.actual` byte diff, manually inspect the diff:
|
||||||
|
- If the diff is ONLY the qualifier-trim behaviour we just added
|
||||||
|
(and the snapshot input file does have qualified intra-module
|
||||||
|
Type::Cons), regenerate the snapshot:
|
||||||
|
`cargo run -p ail -- prose examples/<stem>.ail.json > examples/<stem>.prose.txt`.
|
||||||
|
Confirm the diff post-regenerate is what you expect.
|
||||||
|
- If the diff is OTHERWISE unexpected, that is a bug — investigate.
|
||||||
|
|
||||||
|
For each snapshot whose `.actual` file appeared, decide whether
|
||||||
|
to regenerate or revert.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Create the `ordering_match` snapshot**
|
||||||
|
|
||||||
|
Run: `cargo run -p ail -- prose examples/ordering_match.ail.json > examples/ordering_match.prose.txt`
|
||||||
|
|
||||||
|
Expected: command succeeds, the output file is non-empty and
|
||||||
|
contains `prelude.Ordering` (cross-module qualifier preserved
|
||||||
|
because `prelude` ≠ `ordering_match`).
|
||||||
|
|
||||||
|
- [ ] **Step 9: Register the new snapshot test**
|
||||||
|
|
||||||
|
Edit `crates/ailang-prose/tests/snapshot.rs`. Append:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn snapshot_ordering_match() {
|
||||||
|
check_snapshot("ordering_match");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 10: Run the prose test suite**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace -p ailang-prose`
|
||||||
|
|
||||||
|
Expected: all pass, including the new `snapshot_ordering_match`.
|
||||||
|
|
||||||
|
- [ ] **Step 11: Run the full test suite**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace`
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
- [ ] **Step 12: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add crates/ailang-prose/src/lib.rs \
|
||||||
|
crates/ailang-prose/tests/snapshot.rs \
|
||||||
|
examples/ordering_match.prose.txt
|
||||||
|
git commit -m "iter ct.4.3: prose qualifier-trim-on-print + ordering_match snapshot"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Recreate `compare_primitives_smoke.ail.json` + E2E
|
||||||
|
|
||||||
|
This task resumes the paused iter 23.3 work (TaskList #15+#16):
|
||||||
|
recreate the end-to-end fixture that demonstrates the
|
||||||
|
iter-23.1/23.3 bug closure, run it through the full pipeline,
|
||||||
|
and lock the IR-shape integration assertion.
|
||||||
|
|
||||||
|
The fixture: consumer module `compare_primitives_smoke` imports
|
||||||
|
prelude, calls `Ord.compare` on 9 pairs (3 each of Int/Bool/Str
|
||||||
|
arranged as LT-case / EQ-case / GT-case), pattern-matches each
|
||||||
|
result on `LT | EQ | GT`, prints 1 / 2 / 3 respectively. Expected
|
||||||
|
stdout: `1\n2\n3\n1\n2\n3\n1\n2\n3\n`.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `examples/compare_primitives_smoke.ail.json` — the
|
||||||
|
fixture.
|
||||||
|
- Modify: `crates/ail/tests/e2e.rs` — append the E2E test and the
|
||||||
|
IR-shape integration test.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Author the canonical-form fixture**
|
||||||
|
|
||||||
|
Create `examples/compare_primitives_smoke.ail.json` with the
|
||||||
|
structure below. The fixture has one fn `main` with a `body`
|
||||||
|
that is a sequence of 9 `do io/print_int` statements; each
|
||||||
|
`print_int` wraps a `match` on `(call prelude.compare A B)`
|
||||||
|
returning a `prelude.Ordering`. Use qualified
|
||||||
|
`prelude.Ordering` throughout (canonical post-ct.1 since the
|
||||||
|
type's owner ≠ this file's module).
|
||||||
|
|
||||||
|
For brevity, build the fixture incrementally — start with one
|
||||||
|
case, verify it loads, then add the rest. The shape of a single
|
||||||
|
case (Int, LT):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "do",
|
||||||
|
"op": "io/print_int",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"t": "match",
|
||||||
|
"scrutinee": {
|
||||||
|
"t": "app",
|
||||||
|
"fn": { "t": "var", "name": "prelude.compare" },
|
||||||
|
"args": [
|
||||||
|
{ "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
||||||
|
{ "t": "lit", "lit": { "kind": "int", "value": 2 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"arms": [
|
||||||
|
{
|
||||||
|
"pat": { "p": "ctor", "ctor": "LT", "fields": [] },
|
||||||
|
"body": { "t": "lit", "lit": { "kind": "int", "value": 1 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pat": { "p": "ctor", "ctor": "EQ", "fields": [] },
|
||||||
|
"body": { "t": "lit", "lit": { "kind": "int", "value": 2 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pat": { "p": "ctor", "ctor": "GT", "fields": [] },
|
||||||
|
"body": { "t": "lit", "lit": { "kind": "int", "value": 3 } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Compose 9 such blocks (Int LT/EQ/GT, Bool LT/EQ/GT, Str LT/EQ/GT)
|
||||||
|
in a `seq` chain. For Bool, use `bool` literals (`{"kind": "bool", "value": false}`);
|
||||||
|
for Str, use `str` literals (`{"kind": "str", "value": "a"}`).
|
||||||
|
|
||||||
|
The fn `main` returns `Unit` with `IO` effect:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema": "ailang/v0",
|
||||||
|
"name": "compare_primitives_smoke",
|
||||||
|
"imports": [{ "module": "prelude" }],
|
||||||
|
"defs": [
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "main",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [],
|
||||||
|
"ret": { "k": "con", "name": "Unit" },
|
||||||
|
"effects": ["IO"]
|
||||||
|
},
|
||||||
|
"params": [],
|
||||||
|
"body": { "t": "seq", "lhs": <case 1>, "rhs": { "t": "seq", "lhs": <case 2>, "rhs": ... <case 9> ... } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The 9 cases pair-pattern (which value, which compare, expected output):
|
||||||
|
|
||||||
|
| # | type | A | B | expected match | print |
|
||||||
|
|---|------|--------|--------|----------------|-------|
|
||||||
|
| 1 | Int | 1 | 2 | LT | 1 |
|
||||||
|
| 2 | Int | 1 | 1 | EQ | 2 |
|
||||||
|
| 3 | Int | 2 | 1 | GT | 3 |
|
||||||
|
| 4 | Bool | false | true | LT | 1 |
|
||||||
|
| 5 | Bool | false | false | EQ | 2 |
|
||||||
|
| 6 | Bool | true | false | GT | 3 |
|
||||||
|
| 7 | Str | "a" | "b" | LT | 1 |
|
||||||
|
| 8 | Str | "a" | "a" | EQ | 2 |
|
||||||
|
| 9 | Str | "b" | "a" | GT | 3 |
|
||||||
|
|
||||||
|
You'll write this manually as one big JSON file. Use 2-space
|
||||||
|
indentation and standard `serde_json`-canonical key ordering
|
||||||
|
(the validator + manifest test will fail otherwise; alternatively,
|
||||||
|
run the file through `cargo run -p ail -- manifest examples/compare_primitives_smoke.ail.json`
|
||||||
|
to canonicalise once it loads, then commit the canonicalised
|
||||||
|
form).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify the fixture loads + typechecks**
|
||||||
|
|
||||||
|
Run: `cargo run -p ail -- check examples/compare_primitives_smoke.ail.json`
|
||||||
|
|
||||||
|
Expected: exit 0, no diagnostics. If validator complains about
|
||||||
|
canonical form (e.g. bare `Ordering` somewhere), fix and re-run.
|
||||||
|
If typechecker complains about an unknown ctor or pattern type
|
||||||
|
mismatch, that is the iter-23.1 bug — investigate (it should
|
||||||
|
NOT happen post-ct.2).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify the fixture runs end-to-end**
|
||||||
|
|
||||||
|
Run: `cargo run -p ail -- run examples/compare_primitives_smoke.ail.json`
|
||||||
|
|
||||||
|
Expected: stdout exactly `1\n2\n3\n1\n2\n3\n1\n2\n3\n` (nine
|
||||||
|
lines, no extra newline at the very end depending on the runtime
|
||||||
|
printer convention — match whatever the existing
|
||||||
|
`ordering_match_via_prelude_prints_1` test asserts for trailing
|
||||||
|
newline).
|
||||||
|
|
||||||
|
If the run fails, the iter-23.1 bug closure is NOT complete —
|
||||||
|
escalate to orchestrator.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Append the E2E test to e2e.rs**
|
||||||
|
|
||||||
|
Edit `crates/ail/tests/e2e.rs`. Append after the
|
||||||
|
`ordering_match_via_prelude_prints_1` test (around line 920):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Iter 23.3 Task 4 (resumed in ct.4): the end-to-end
|
||||||
|
/// demonstration that the iter-23.1 cross-module Type::Con
|
||||||
|
/// mismatch bug is closed. `examples/compare_primitives_smoke.ail.json`
|
||||||
|
/// imports prelude, calls `prelude.compare` on three pairs each
|
||||||
|
/// of Int / Bool / Str (arranged LT / EQ / GT per type), and
|
||||||
|
/// pattern-matches the result. Expected stdout encodes the
|
||||||
|
/// 9-case decision lattice as `1\n2\n3` × 3.
|
||||||
|
///
|
||||||
|
/// Pre-ct.1 / pre-ct.2, the bare `Ordering` returned by
|
||||||
|
/// `compare` mismatched the qualified `prelude.Ordering`
|
||||||
|
/// scrutinee at the user-side pattern lookup. Post-canonical-
|
||||||
|
/// type-names, the scrutinee is qualified throughout, the
|
||||||
|
/// pattern lookup is type-driven, and the run completes cleanly.
|
||||||
|
#[test]
|
||||||
|
fn compare_primitives_smoke_prints_1_2_3_thrice() {
|
||||||
|
let stdout = build_and_run("compare_primitives_smoke.ail.json");
|
||||||
|
assert_eq!(stdout, "1\n2\n3\n1\n2\n3\n1\n2\n3\n");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Append the IR-shape integration test**
|
||||||
|
|
||||||
|
Append immediately after the E2E test:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Iter 23.3 Task 4 / IR-shape: the mono pass must emit
|
||||||
|
/// `compare__Int`, `compare__Bool`, and `compare__Str` symbols
|
||||||
|
/// driven by the three `prelude.compare` call sites in the
|
||||||
|
/// smoke fixture. Codegen's `try_emit_primitive_instance_body`
|
||||||
|
/// intercepts each one (iter 23.3.2). This test pins the
|
||||||
|
/// presence of those symbols in the emitted IR so a future
|
||||||
|
/// refactor cannot silently drop one.
|
||||||
|
#[test]
|
||||||
|
fn compare_primitives_smoke_emits_compare_mono_symbols() {
|
||||||
|
let ir = emit_ir("compare_primitives_smoke.ail.json");
|
||||||
|
assert!(
|
||||||
|
ir.contains("compare__Int") || ir.contains("@compare__Int"),
|
||||||
|
"compare__Int symbol missing from emitted IR"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ir.contains("compare__Bool") || ir.contains("@compare__Bool"),
|
||||||
|
"compare__Bool symbol missing from emitted IR"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ir.contains("compare__Str") || ir.contains("@compare__Str"),
|
||||||
|
"compare__Str symbol missing from emitted IR"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If a helper `emit_ir(fixture: &str) -> String` does not already
|
||||||
|
exist in `e2e.rs`, add it adjacent to `build_and_run` (it should
|
||||||
|
invoke `cargo run -p ail -- emit-ir <fixture>` and capture
|
||||||
|
stdout). Use the existing `build_and_run`'s implementation as a
|
||||||
|
template.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the new E2E + IR tests**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace -p ail compare_primitives_smoke_prints compare_primitives_smoke_emits`
|
||||||
|
|
||||||
|
Expected: BOTH PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run the full test suite**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace`
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add examples/compare_primitives_smoke.ail.json crates/ail/tests/e2e.rs
|
||||||
|
git commit -m "iter ct.4.4: compare_primitives_smoke E2E fixture + IR-shape test (closes iter 23.3 Task 4)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Closing checks
|
||||||
|
|
||||||
|
- [ ] **Step C1: Bench script smoke run**
|
||||||
|
|
||||||
|
Run: `bash bench/run.sh` (or whichever entry point the project
|
||||||
|
uses; consult `bench/README.md` or `skills/audit/SKILL.md`).
|
||||||
|
|
||||||
|
Expected: exit 0, or a ratified non-zero per the audit-skill
|
||||||
|
policy. No new regressions vs. baseline.
|
||||||
|
|
||||||
|
- [ ] **Step C2: Confirm all acceptance criteria from the spec**
|
||||||
|
|
||||||
|
Verify each item in `docs/specs/2026-05-10-canonical-type-names.md`
|
||||||
|
§ "Acceptance criteria":
|
||||||
|
|
||||||
|
1. ✅ `load_workspace` validates every `Type::Con` (ct.1).
|
||||||
|
2. ✅ Every fixture migrated to qualified form (ct.1).
|
||||||
|
3. ✅ Four obsolete mechanisms deleted (ct.2.2, ct.2.3, ct.3.1, ct.3.2).
|
||||||
|
4. ✅ `qualify_local_types` audited and applied uniformly (ct.2.1).
|
||||||
|
5. ✅ `cargo build --workspace && cargo test --workspace` green (this commit).
|
||||||
|
6. ✅ Iter 23.3 Task 4+5 resumed and complete (ct.4 Task 4 above).
|
||||||
|
7. ✅ DESIGN.md amended (ct.4 Task 1).
|
||||||
|
8. ✅ JOURNAL entry (ct.4 Task C3 below).
|
||||||
|
9. ⏳ Bench scripts exit 0 (verify via C1).
|
||||||
|
|
||||||
|
- [ ] **Step C3: JOURNAL entry**
|
||||||
|
|
||||||
|
Append to `docs/JOURNAL.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 2026-05-11 — Iteration ct.4: canonical-type-names milestone close
|
||||||
|
|
||||||
|
Four tasks landed, closing the milestone:
|
||||||
|
|
||||||
|
- **ct.4.1**: amended DESIGN.md Decision 2 with the canonical
|
||||||
|
Type::Con name scoping rule (bare = local, qualified =
|
||||||
|
cross-module, primitives bare). Updated the regression-pin
|
||||||
|
citation at the surrounding "hashes stay bit-identical"
|
||||||
|
paragraph to name the two new ct.4 pins.
|
||||||
|
- **ct.4.2**: added two new pin tests in
|
||||||
|
`crates/ailang-core/src/hash.rs` —
|
||||||
|
`ct4_migrated_fixtures_have_canonical_form_hashes` (locks the
|
||||||
|
post-migration hashes of `ordering_match.ail.json` and
|
||||||
|
`test_22b1_dup_a.ail.json`) and
|
||||||
|
`ct4_unmigrated_fixtures_remain_bit_identical` (re-asserts the
|
||||||
|
unchanged `sum`/`list` hashes).
|
||||||
|
- **ct.4.3**: added qualifier-trim-on-print to the prose
|
||||||
|
printer. `write_type` (and `Term::Ctor.type_name` writing)
|
||||||
|
now trims a qualifier whose owner matches the current file's
|
||||||
|
module. The `owning_module` context is threaded from
|
||||||
|
`write_module` down through the call chain. Two unit tests
|
||||||
|
pin the trim and the cross-module preserve behaviours; the
|
||||||
|
`ordering_match.prose.txt` snapshot is committed.
|
||||||
|
- **ct.4.4**: recreated `examples/compare_primitives_smoke.ail.json`
|
||||||
|
(the iter-23.3 Task 4 demonstration fixture). 9 cases across
|
||||||
|
Int / Bool / Str, expected stdout `1\n2\n3` × 3. Two new
|
||||||
|
E2E tests in `crates/ail/tests/e2e.rs` —
|
||||||
|
`compare_primitives_smoke_prints_1_2_3_thrice` (output
|
||||||
|
assertion) and `compare_primitives_smoke_emits_compare_mono_symbols`
|
||||||
|
(IR-shape assertion that mono emits compare__Int /
|
||||||
|
compare__Bool / compare__Str symbols).
|
||||||
|
|
||||||
|
The canonical-type-names milestone closes:
|
||||||
|
- The structural invariant: every `Type::Con` in canonical
|
||||||
|
`.ail.json` is bare (= local) or qualified
|
||||||
|
(`<owner>.<TypeName>`, with primitives bare).
|
||||||
|
- The four obsolete imports-fallback / overlay mechanisms named
|
||||||
|
in the ct.1 catalogue are gone (Pattern::Ctor, Term::Ctor
|
||||||
|
synth, codegen `lookup_ctor_by_type`, mono
|
||||||
|
`apply_per_module_ctor_index_overlay`'s ctor_index half).
|
||||||
|
- The iter-23.1-Task-4 bug is empirically closed: the smoke
|
||||||
|
fixture compiles, runs, and prints the expected 9-line output
|
||||||
|
end-to-end without any further compiler changes.
|
||||||
|
|
||||||
|
Workspace at milestone close: full `cargo build --workspace` and
|
||||||
|
`cargo test --workspace` green, 453+ tests passing
|
||||||
|
(ct.4 adds 4 new tests: 2 pin + 2 prose + 2 E2E, minus or plus
|
||||||
|
any shifts in other test count).
|
||||||
|
|
||||||
|
The skill-system pipeline calls for `audit` next (drift review +
|
||||||
|
bench regression + rustdoc), then `fieldtest` (real-world `.ailx`
|
||||||
|
examples authored against DESIGN.md only).
|
||||||
|
|
||||||
|
Out of scope, deferred:
|
||||||
|
- The codegen `lookup_ctor_in_pattern` helper (imports-walk, but
|
||||||
|
no scrutinee type to type-anchor; a later iter can plumb Type
|
||||||
|
through pattern lowering if desired).
|
||||||
|
- `check_in_workspace`'s analogous per-module overlay in
|
||||||
|
`crates/ailang-check/src/lib.rs:1234` (its env.ctor_index half
|
||||||
|
serves the duplicate-detection diagnostic at workspace-build
|
||||||
|
time, not the runtime ctor lookup; not in the canonical-type-
|
||||||
|
names scope).
|
||||||
|
- Class names (`ClassDef.name`, `InstanceDef.class`, etc.) are
|
||||||
|
intentionally bare and workspace-flat post-ct.1; type-driven
|
||||||
|
method dispatch is its own milestone (already named in
|
||||||
|
`docs/roadmap.md` as a P2 item).
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step C4: Commit the JOURNAL entry**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/JOURNAL.md
|
||||||
|
git commit -m "journal: ct.4 canonical-type-names milestone close"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step C5: Optional roadmap update**
|
||||||
|
|
||||||
|
If the user wants the milestone removed from
|
||||||
|
`docs/roadmap.md` (or marked done with a checkmark), make that
|
||||||
|
edit now and commit alongside the JOURNAL entry. Otherwise leave
|
||||||
|
the roadmap as-is — the orchestrator typically prunes finished
|
||||||
|
entries in a small follow-up commit.
|
||||||
Reference in New Issue
Block a user