plan: 24.2 — class Show + 4 instances + 22b migration, 8 tasks
Plan for iter 24.2. Tasks 1-2: preemptive 22b-Show → TShow/tshow rename across 21 fixtures + 2 Rust test files (the recon-corrected count vs spec's '~14' undercount). Task 3: insert 5 new top-level defs (class Show + 4 primitive instances Int/Bool/Str/Float) at prelude.ail.json:223 before fn ne. Task 4: mono-synthesis existence test for show__T. Task 5: dispatch-singleton pin (show vs tshow, both Step-2 singletons post-migration). Task 6: hash-stability pin extension. Task 7: DESIGN.md §Prelude(built-in) classes amendment. Task 8: integration + bench verification. Recon-Boss reconciliation captured in Pre-flight notes: 21 fixtures (not 14), prelude line 223 insertion point, str_clone-reachability verified by Task 3 round-trip.
This commit is contained in:
@@ -0,0 +1,765 @@
|
||||
# Iter 24.2 — class Show + 4 instances + 22b migration — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/2026-05-13-24-show-print.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
||||
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Ship `class Show a where show : (a borrow) -> Str` plus four primitive instances (Int/Bool/Str/Float) in the prelude module, and migrate the 21 user-Show fixtures + 2 Rust test files to the test-internal `TShow`/`tshow` convention so the workspace has zero ambiguous bare-`show` call sites post-merge.
|
||||
|
||||
**Architecture:** Iter 24.2 is a structural addition to `examples/prelude.ail.json` (5 new top-level defs) plus a string-rename pass across 21 fixture files and 2 Rust test files. The migration runs **before** the prelude additions so the workspace stays green throughout — adding `prelude.Show` while the user-Show fixtures still own the `show` method name would cause every `cargo test --workspace` mid-iter to fail with Multi-class ambiguity diagnostics at every `show x` call site in the 22b corpus. Three new in-crate tests (mono-synthesis existence, dispatch-singleton pin, hash-stability) verify the new mono symbols materialise with stable hashes and that bare `show` / bare `tshow` route to their unique candidate classes via Step-2 singleton resolution. The DESIGN.md amendment (~10 lines) updates §"Prelude (built-in) classes" to record Show as shipped and to retain `print`-rewire as the iter-24.3 carryforward.
|
||||
|
||||
**Tech Stack:** `ailang-core` (workspace loader, hash), `ailang-check` (mono synthesis, dispatcher), prelude JSON, Rust integration tests at `crates/ail/tests`.
|
||||
|
||||
---
|
||||
|
||||
## Pre-flight notes (Boss-ratified post-recon)
|
||||
|
||||
**Recon-corrected target count.** The spec's "~14 fixtures" was an undercount based on `grep -l '"name": "Show"' examples/test_22b*_*.ail.json` (class-definition matches only). The recon enumeration shows **21 .ail.json files** affected — 18 with class def or instance def, plus 3 cross-module-companion fixtures (`test_22b2_xmod_instance_present`, `test_22b2_xmod_missing_constraint`, `test_22b2_xmod_no_instance`) that carry call-sites referencing `<modname>.Show`. The full target list is enumerated in Task 1.
|
||||
|
||||
**Insertion point for new Show defs in `prelude.ail.json`.** Line 223 (immediately before `"kind": "fn", "name": "ne"`). The five new defs (class Show + 4 instances) push the polymorphic helpers `ne`/`lt`/`le`/`gt`/`ge` down. Milestone-23 mono-symbol body hashes pinned at `crates/ail/tests/mono_hash_stability.rs:34-41` are keyed on `Def::Fn.body` content, not workspace def order, so insertion is expected hash-neutral.
|
||||
|
||||
**`str_clone` reachability in instance lambda body.** `str_clone` is installed in `crates/ailang-check/src/builtins.rs:194-224` with `ret_mode: Own`, just like `int_to_str`. Recon flagged this as needing first-round-trip verification because no existing prelude instance body invokes a runtime primitive directly (existing Eq instances use `==`-operator + codegen intercept; existing Ord instances use `compare` placeholder bodies). Task 3 Step 5 includes the round-trip test that fires this verification implicitly — if `str_clone` is not reachable as `Term::Var` in instance-lambda position the round-trip test FAILS and the implementer surfaces the gap to the Boss.
|
||||
|
||||
**mq1 fixtures stay unchanged.** `examples/mq1_xmod_constraint_class.ail.json` and `..._dep.ail.json` reference `mq1_xmod_constraint_class_dep.Show` (cross-module, fully qualified) and own a `class Show` in the `_dep` module, but ship **no `show` call sites** (verified via `grep '"t": "app".*"show"\|"fn":.*"show"'`). The xmod constraint-class reference resolves via the explicit qualifier; no dispatch ambiguity arises. These two fixtures stay unchanged in 24.2.
|
||||
|
||||
---
|
||||
|
||||
## Files this plan creates or modifies
|
||||
|
||||
**Create:**
|
||||
- `crates/ailang-check/tests/show_dispatch_pin.rs` — new unit pin: bare `show` and bare `tshow` both Step-2 singletons against a workspace carrying `prelude.Show` + user `TShow`.
|
||||
- `examples/show_mono_pin_smoke.ail.json` — new fixture exercising `show 1`, `show true`, `show "x"`, `show 1.5` so the mono pass materialises all 4 `show__T` symbols.
|
||||
- `crates/ail/tests/show_mono_synthesis.rs` — new integration test loading the fixture above and asserting `show__Int/Bool/Str/Float` exist in `prelude` post-mono.
|
||||
|
||||
**Modify:**
|
||||
- `examples/prelude.ail.json:223` — insert 5 new top-level defs (class Show + 4 instances) before line 223.
|
||||
- `docs/DESIGN.md:1888-1899` — amend §"Prelude (built-in) classes" milestone-23 paragraph: drop "Show" from the still-deferred list and add a milestone-24 paragraph naming Show + 4 primitive instances (including Float) + the per-instance body-shape (lambda invoking corresponding `*_to_str` primitive).
|
||||
- `crates/ail/tests/mono_hash_stability.rs:34-41` — extend `pins: &[(&str, &str)]` table with 4 entries for `show__Int/Bool/Str/Float` (placeholder hashes captured from first-run failure, then updated to actual values).
|
||||
- `examples/mono_hash_pin_smoke.ail.json` — extend fixture to exercise `show` calls so `show__T` hashes pin via the same load (alternative: pin via the new `show_mono_pin_smoke.ail.json` from a second load step; see Task 6 Step 2 — implementer picks).
|
||||
- 21 fixture files under `examples/test_22b*_*.ail.json` — string rename (enumerated in Task 1).
|
||||
- `crates/ail/tests/typeclass_22b2.rs:1-383` — `"Show"` → `"TShow"`, `"show"` → `"tshow"` in literals + prose.
|
||||
- `crates/ail/tests/typeclass_22b3.rs:1-811` — same pattern.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Migrate 22b user-Show fixtures (preemptive rename)
|
||||
|
||||
**Files:**
|
||||
- Modify: `examples/test_22b1_dup_a.ail.json`
|
||||
- Modify: `examples/test_22b1_dup_b.ail.json`
|
||||
- Modify: `examples/test_22b1_dup_classmod.ail.json`
|
||||
- Modify: `examples/test_22b1_dup_same_module.ail.json`
|
||||
- Modify: `examples/test_22b1_orphan_class.ail.json`
|
||||
- Modify: `examples/test_22b1_orphan_third.ail.json`
|
||||
- Modify: `examples/test_22b1_orphan_third_classmod.ail.json`
|
||||
- Modify: `examples/test_22b2_class_method_lookup.ail.json`
|
||||
- Modify: `examples/test_22b2_constraint_declared.ail.json`
|
||||
- Modify: `examples/test_22b2_instance_present.ail.json`
|
||||
- Modify: `examples/test_22b2_missing_constraint.ail.json`
|
||||
- Modify: `examples/test_22b2_no_instance.ail.json`
|
||||
- Modify: `examples/test_22b2_two_fns_missing_constraint.ail.json`
|
||||
- Modify: `examples/test_22b2_xmod_classmod.ail.json`
|
||||
- Modify: `examples/test_22b2_xmod_classmod_noinst.ail.json`
|
||||
- Modify: `examples/test_22b2_xmod_instance_present.ail.json`
|
||||
- Modify: `examples/test_22b2_xmod_missing_constraint.ail.json`
|
||||
- Modify: `examples/test_22b2_xmod_no_instance.ail.json`
|
||||
- Modify: `examples/test_22b3_dup_call_sites.ail.json`
|
||||
- Modify: `examples/test_22b3_no_call_sites.ail.json`
|
||||
- Modify: `examples/test_22b3_shadow_class_method.ail.json`
|
||||
|
||||
- [ ] **Step 1: Verify the target set matches the empirical count.**
|
||||
|
||||
Run: `grep -l '"name": "Show"\|"class": "Show"\|"class".*\.Show"\|"name": "show"' examples/test_22b*_*.ail.json | sort -u | wc -l`
|
||||
|
||||
Expected output: `21`
|
||||
|
||||
If the count is not 21, the spec-vs-empirical reconciliation needs to be re-done. Either: (a) new fixtures landed since recon (stop, surface to Boss); or (b) recon missed a fixture (re-run grep with looser pattern and reconcile). Do not proceed past Step 1 with a count mismatch.
|
||||
|
||||
- [ ] **Step 2: Per-file rename — replace all four literal strings.**
|
||||
|
||||
For each of the 21 files above, run sed-equivalent replacements:
|
||||
- `"name": "Show"` → `"name": "TShow"`
|
||||
- `"class": "Show"` → `"class": "TShow"`
|
||||
- `"class": "<modname>.Show"` → `"class": "<modname>.TShow"` for every `<modname>` (concretely: `test_22b1_dup_classmod`, `test_22b1_orphan_third_classmod`, `test_22b2_xmod_classmod`, plus any companion modules)
|
||||
- `"name": "show"` → `"name": "tshow"`
|
||||
|
||||
Use Edit tool with `replace_all: true` per file, four edits per file (one per pattern).
|
||||
|
||||
False-positive guard: scan each file post-rename for any remaining `Show` or `show` literal that should NOT have been renamed (e.g. inside a comment or a string literal that is itself test data). The spec's open commitment (line 502-504) states no such false-positives are expected from visual scan, but per-file verification is required.
|
||||
|
||||
- [ ] **Step 3: Run round-trip on each migrated fixture.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast -p ail --test fixtures_roundtrip 2>&1 | tail -30`
|
||||
|
||||
Expected: all migrated fixtures parse + canonicalise bit-stable (test names contain the fixture's module name; all 21 must appear in the green list).
|
||||
|
||||
If `fixtures_roundtrip` is not the right test name, run: `cargo test --workspace --no-fail-fast 2>&1 | grep -E 'test_22b.*(ok|FAILED)' | head -30`
|
||||
|
||||
Expected: zero `FAILED` lines.
|
||||
|
||||
- [ ] **Step 4: Run the typeclass_22b{2,3} tests to confirm they fail (RED proof for Task 2).**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast -p ail --test typeclass_22b2 --test typeclass_22b3 2>&1 | tail -20`
|
||||
|
||||
Expected: FAIL — the tests still assert on `"Show"`/`"show"` literals but the fixtures now declare `TShow`/`tshow`. This is the RED state Task 2 fixes.
|
||||
|
||||
If the tests pass at Step 4, the fixture migration did not affect the test workload (suggests either the tests don't actually exercise the migrated literals, or the rename was incomplete). Surface to Boss before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Migrate 22b Rust test files
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ail/tests/typeclass_22b2.rs`
|
||||
- Modify: `crates/ail/tests/typeclass_22b3.rs`
|
||||
|
||||
- [ ] **Step 1: Read typeclass_22b2.rs to identify every occurrence.**
|
||||
|
||||
Run: `grep -n '"Show"\|"show"' crates/ail/tests/typeclass_22b2.rs | head -40`
|
||||
|
||||
Expected: 30 occurrences (per recon). Categorise:
|
||||
- string-literal class names in `class_method_class("...", "show")` calls
|
||||
- string-literal method names in `class_method` / `has_class_method` calls
|
||||
- expected-diagnostic substrings ("class Show ...", "shadow show ...")
|
||||
- mono-symbol literals: `"show__Int"`, `"show__Bool"`, etc. — these become `"tshow__Int"`/`"tshow__Bool"`/etc.
|
||||
- rustdoc prose narrating `class Show` / `show x` semantics
|
||||
|
||||
- [ ] **Step 2: Replace literals + mono-symbols in typeclass_22b2.rs.**
|
||||
|
||||
Using Edit tool with `replace_all: true`:
|
||||
- `"Show"` → `"TShow"`
|
||||
- `"show"` → `"tshow"`
|
||||
- `"show__Int"` → `"tshow__Int"` (and `Bool`, `Str` variants if present)
|
||||
- `class Show` → `class TShow` (in rustdoc)
|
||||
- `show x` → `tshow x` (in rustdoc)
|
||||
|
||||
Order matters: do the `show__T` mono-symbol replaces first (longer match), then the bare `"show"` replace (shorter match). Otherwise `replace_all` on `"show"` will create `"tshow__Int"` *and also* corrupt any string already migrated.
|
||||
|
||||
Concretely:
|
||||
1. Edit: `"show__Int"` → `"tshow__Int"` (replace_all)
|
||||
2. Edit: `"show__Bool"` → `"tshow__Bool"` (replace_all)
|
||||
3. Edit: `"show__Str"` → `"tshow__Str"` (replace_all)
|
||||
4. Edit: `"Show"` → `"TShow"` (replace_all)
|
||||
5. Edit: `"show"` → `"tshow"` (replace_all)
|
||||
6. Edit: `class Show` → `class TShow` (replace_all)
|
||||
7. Edit: `show x` → `tshow x` (replace_all)
|
||||
|
||||
- [ ] **Step 3: Repeat Step 1-2 for typeclass_22b3.rs.**
|
||||
|
||||
Expected occurrence count: 49 (per recon). Same 7-step ordered Edit sequence.
|
||||
|
||||
Additional patterns to watch (per recon line range): `count == 1, "exactly one show__Int across the workspace"` at line 397 — the assertion message string itself contains `show__Int` and renames to `tshow__Int`.
|
||||
|
||||
- [ ] **Step 4: Run typeclass_22b2 + typeclass_22b3 tests, verify GREEN.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast -p ail --test typeclass_22b2 --test typeclass_22b3 2>&1 | tail -10`
|
||||
|
||||
Expected: all tests pass. If any fails, the rename was either incomplete or hit a false-positive in a function name or rustdoc that shouldn't have changed. Diagnose: `cargo test ... -- --nocapture | head -50`.
|
||||
|
||||
- [ ] **Step 5: Run full workspace test to confirm no broader regression.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast 2>&1 | tail -5`
|
||||
|
||||
Expected: `test result: ok. <N> passed; 0 failed`. The total count should match pre-iter baseline (548 from mq.tidy close).
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add `class Show` + 4 primitive instances to prelude.ail.json
|
||||
|
||||
**Files:**
|
||||
- Modify: `examples/prelude.ail.json` — insert 5 defs before line 223 (the `"kind": "fn", "name": "ne"` entry).
|
||||
|
||||
- [ ] **Step 1: Read prelude.ail.json lines 218-225 to confirm insertion point.**
|
||||
|
||||
Read lines 218-225. Expected: line 222 is the closing `},` of the last Ord-Str instance, line 223 begins the `ne` fn def with `{ "kind": "fn"`.
|
||||
|
||||
If the structure differs (e.g. trailing comma issues, JSON shape changes), the implementer adjusts the insertion line accordingly; the rule is "after the last Ord instance, before the first polymorphic free-fn".
|
||||
|
||||
- [ ] **Step 2: Insert the class def.**
|
||||
|
||||
Use Edit tool to add the following entry BEFORE the existing `{ "kind": "fn", "name": "ne", ...` at line 223:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": "class",
|
||||
"name": "Show",
|
||||
"param": "a",
|
||||
"doc": "Producer of a human-readable Str representation. Ships in milestone 24 with primitive instances for Int/Bool/Str/Float; user types declare their own instance.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"param_modes": ["borrow"],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
The trailing comma is required because `ne` follows.
|
||||
|
||||
- [ ] **Step 3: Insert `instance Show Int`.**
|
||||
|
||||
Immediately after the class def, before `ne`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "Show",
|
||||
"type": { "k": "con", "name": "Int" },
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x"],
|
||||
"paramTypes": [{ "k": "con", "name": "Int" }],
|
||||
"retType": { "k": "con", "name": "Str" },
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "int_to_str" },
|
||||
"args": [{ "t": "var", "name": "x" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Insert `instance Show Bool`, `instance Show Str`, `instance Show Float`.**
|
||||
|
||||
Immediately following Step-3's instance, three more entries:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "Show",
|
||||
"type": { "k": "con", "name": "Bool" },
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x"],
|
||||
"paramTypes": [{ "k": "con", "name": "Bool" }],
|
||||
"retType": { "k": "con", "name": "Str" },
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "bool_to_str" },
|
||||
"args": [{ "t": "var", "name": "x" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "Show",
|
||||
"type": { "k": "con", "name": "Str" },
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x"],
|
||||
"paramTypes": [{ "k": "con", "name": "Str" }],
|
||||
"retType": { "k": "con", "name": "Str" },
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "str_clone" },
|
||||
"args": [{ "t": "var", "name": "x" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "Show",
|
||||
"type": { "k": "con", "name": "Float" },
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x"],
|
||||
"paramTypes": [{ "k": "con", "name": "Float" }],
|
||||
"retType": { "k": "con", "name": "Str" },
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "float_to_str" },
|
||||
"args": [{ "t": "var", "name": "x" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run prelude round-trip via the workspace loader.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast 2>&1 | grep -E 'prelude.*roundtrip|prelude.*hash|primitive_eq_ord|test result' | head -20`
|
||||
|
||||
Expected: existing prelude round-trip and milestone-23 hash-stability tests pass. The `primitive_eq_ord_mono_symbol_hashes_stay_bit_identical` test (at `crates/ail/tests/mono_hash_stability.rs:19`) should stay green because the new Show defs do not perturb the existing Eq/Ord mono body hashes.
|
||||
|
||||
If the prelude round-trip fails with a JSON parse error, the inserted block has a syntax issue (trailing comma, brace mismatch). Diagnose by running `python3 -c "import json; json.load(open('examples/prelude.ail.json'))"` and reading the resulting error.
|
||||
|
||||
If `primitive_eq_ord_mono_symbol_hashes_stay_bit_identical` fails, the new Show defs are perturbing existing mono-symbol bodies (unexpected — surface to Boss).
|
||||
|
||||
- [ ] **Step 6: Run typecheck on the prelude to confirm Show defs are well-formed.**
|
||||
|
||||
Run: `cargo run --bin ail -- check examples/prelude.ail.json 2>&1 | head -10`
|
||||
|
||||
Expected: exit 1 with "module name 'prelude' is reserved (auto-injected by the loader)" — this is the *correct* exit because the prelude is loader-auto-injected, not a workspace entry. The error indicates the file loaded far enough to enter the loader's name-check.
|
||||
|
||||
The actual typecheck of the prelude happens indirectly when ANY workspace test loads the auto-injected prelude. So Step 5's workspace-test run is the load-bearing prelude typecheck gate; this Step 6 is just confirming the JSON syntax is parseable.
|
||||
|
||||
If Step 6 exits with a parse error rather than the reserved-name error, the JSON has structural issues — fix before proceeding to Task 4.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Mono synthesis existence test for `show__T`
|
||||
|
||||
**Files:**
|
||||
- Create: `examples/show_mono_pin_smoke.ail.json` — workspace fixture exercising all 4 `show` call sites.
|
||||
- Create: `crates/ail/tests/show_mono_synthesis.rs` — assertion that `show__Int/Bool/Str/Float` materialise in `prelude` post-mono.
|
||||
|
||||
- [ ] **Step 1: Write the fixture `examples/show_mono_pin_smoke.ail.json`.**
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "show_mono_pin_smoke",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s1",
|
||||
"value": { "t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "lit", "lit": "int", "value": 42 }] },
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s2",
|
||||
"value": { "t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "lit", "lit": "bool", "value": true }] },
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s3",
|
||||
"value": { "t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "lit", "lit": "str", "value": "x" }] },
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s4",
|
||||
"value": { "t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "lit", "lit": "float", "value": 1.5 }] },
|
||||
"body": { "t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "s1" }] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `do io/print_str s1` at the innermost layer ensures the let-binders are reachable (any one of them suffices; using `s1` is arbitrary). The four `show` calls drive mono synthesis of all 4 `show__T` symbols.
|
||||
|
||||
- [ ] **Step 2: Write the test stub at `crates/ail/tests/show_mono_synthesis.rs`.**
|
||||
|
||||
```rust
|
||||
//! Mono-synthesis existence pin for milestone 24.2's Show class.
|
||||
//!
|
||||
//! Asserts that the four primitive Show instances (Int/Bool/Str/Float)
|
||||
//! cause the mono pass to synthesise `show__Int`, `show__Bool`,
|
||||
//! `show__Str`, `show__Float` as `Def::Fn` entries in the prelude
|
||||
//! post-mono module.
|
||||
|
||||
use ailang_core::ast::Def;
|
||||
use ailang_core::workspace::load_workspace;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn fixture_path() -> PathBuf {
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.pop();
|
||||
d.pop();
|
||||
d.join("examples").join("show_mono_pin_smoke.ail.json")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn primitive_show_mono_symbols_synthesise() {
|
||||
let ws = load_workspace(&fixture_path()).expect("workspace loads");
|
||||
let diags = ailang_check::check_workspace(&ws);
|
||||
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
|
||||
let post_mono = ailang_check::monomorphise_workspace(&ws).expect("mono green");
|
||||
|
||||
let prelude_mod = post_mono
|
||||
.modules
|
||||
.get("prelude")
|
||||
.expect("prelude module present");
|
||||
|
||||
for sym in &["show__Int", "show__Bool", "show__Str", "show__Float"] {
|
||||
let found = prelude_mod
|
||||
.defs
|
||||
.iter()
|
||||
.any(|d| matches!(d, Def::Fn(f) if f.name == *sym));
|
||||
assert!(found, "mono symbol {sym} not found in prelude module");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the test to verify it passes (post Task-3 prelude additions).**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast -p ail --test show_mono_synthesis 2>&1 | tail -10`
|
||||
|
||||
Expected: `test result: ok. 1 passed`.
|
||||
|
||||
If the test fails with "mono symbol show__Int not found", the prelude's `instance Show Int` body is not driving mono synthesis. Inspect post_mono via `cargo test ... -- --nocapture` and surface the gap to the Boss.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Dispatch-singleton pin test (`show` vs `tshow`)
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/ailang-check/tests/show_dispatch_pin.rs` — new pin test.
|
||||
|
||||
- [ ] **Step 1: Read `crates/ailang-check/tests/method_dispatch_pin.rs` for the existing pattern.**
|
||||
|
||||
Run: `head -80 crates/ailang-check/tests/method_dispatch_pin.rs`
|
||||
|
||||
Expected: the file exists and shows the standard pattern (a `mk_workspace_with` helper plus `#[test]` fns asserting `resolve_method_dispatch` returns specific `Resolved("<class>")` variants).
|
||||
|
||||
If the file doesn't exist or the pattern differs substantially, surface to Boss before proceeding — the spec's Testing Strategy referenced this file by name.
|
||||
|
||||
- [ ] **Step 2: Write `crates/ailang-check/tests/show_dispatch_pin.rs`.**
|
||||
|
||||
Mirror the structure of `method_dispatch_pin.rs`. The pin asserts two cases:
|
||||
|
||||
```rust
|
||||
//! Singleton dispatch pin for milestone 24.2's Show class.
|
||||
//!
|
||||
//! Asserts that with `prelude.Show` shipping `show` and a user `TShow`
|
||||
//! shipping `tshow`, the bare method-name dispatcher resolves each via
|
||||
//! Step-2 singleton (each method has exactly one candidate class globally).
|
||||
|
||||
use ailang_check::{check_workspace, env::Env};
|
||||
use ailang_core::workspace::load_workspace;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn fixture_dir() -> PathBuf {
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.pop();
|
||||
d.pop();
|
||||
d.join("examples")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_show_routes_to_prelude_show_via_singleton() {
|
||||
// workspace = auto-loaded prelude (containing class Show)
|
||||
// + a user module with no class-Show or class-TShow.
|
||||
// Asserts: Env.method_to_candidate_classes["show"] == {"prelude.Show"}
|
||||
// (a singleton), so Step-2 of resolve_method_dispatch resolves bare
|
||||
// "show" without needing Steps 3-4.
|
||||
let ws_path = fixture_dir().join("mono_hash_pin_smoke.ail.json");
|
||||
let ws = load_workspace(&ws_path).expect("workspace loads");
|
||||
let diags = check_workspace(&ws);
|
||||
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
|
||||
|
||||
// Construct the env at post-typecheck shape and inspect the index.
|
||||
// The pin asserts only the inverse-index shape; full dispatch trace
|
||||
// already covered by method_dispatch_pin.rs.
|
||||
let env = Env::from_workspace(&ws).expect("env build green");
|
||||
let candidates = env
|
||||
.method_to_candidate_classes
|
||||
.get("show")
|
||||
.expect("method 'show' has candidates");
|
||||
assert_eq!(
|
||||
candidates.len(),
|
||||
1,
|
||||
"post-24.2-migration, 'show' has exactly one candidate class globally: got {candidates:?}"
|
||||
);
|
||||
assert!(
|
||||
candidates.iter().any(|c| c == "prelude.Show"),
|
||||
"candidates do not include prelude.Show: {candidates:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_tshow_routes_to_user_tshow_via_singleton() {
|
||||
// workspace = auto-loaded prelude + the migrated 22b1_dup_classmod
|
||||
// (which now declares class TShow with method tshow).
|
||||
// Asserts: Env.method_to_candidate_classes["tshow"] has exactly one
|
||||
// candidate (the user TShow), workspace-globally.
|
||||
let ws_path = fixture_dir().join("test_22b1_dup_classmod.ail.json");
|
||||
let ws = load_workspace(&ws_path).expect("workspace loads");
|
||||
let diags = check_workspace(&ws);
|
||||
// We expect zero diagnostics; the migrated fixture's class TShow
|
||||
// is structurally valid and ships no instance defs (per its original
|
||||
// intent of testing class-def-only).
|
||||
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
|
||||
|
||||
let env = Env::from_workspace(&ws).expect("env build green");
|
||||
let candidates = env
|
||||
.method_to_candidate_classes
|
||||
.get("tshow")
|
||||
.expect("method 'tshow' has candidates");
|
||||
assert_eq!(
|
||||
candidates.len(),
|
||||
1,
|
||||
"post-22b-migration, 'tshow' is owned by exactly one class globally: got {candidates:?}"
|
||||
);
|
||||
assert!(
|
||||
candidates.iter().any(|c| c.ends_with(".TShow")),
|
||||
"candidates do not include a *.TShow class: {candidates:?}"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
If the `Env::from_workspace` API does not match the snippet above (the implementer should verify the actual signature in `crates/ailang-check/src/env.rs`), adapt to the actual constructor. The load-bearing invariant the test asserts is that `method_to_candidate_classes` has the expected singleton entries; whether the env is built via `Env::from_workspace`, `check_workspace`'s internal env, or another path is an implementation detail.
|
||||
|
||||
If `Env` is not part of the public API of `ailang-check`, the test moves to in-crate (in `crates/ailang-check/src/lib.rs`'s `#[cfg(test)] mod tests`) instead of `crates/ailang-check/tests/`. The Boss preference is the external test file if reachable; in-crate as fallback.
|
||||
|
||||
- [ ] **Step 3: Run the new test.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast -p ailang-check --test show_dispatch_pin 2>&1 | tail -10`
|
||||
|
||||
Expected: 2 passed.
|
||||
|
||||
If the test fails because `Env::from_workspace` is not the correct API: pick the alternative path (in-crate test using the same construction shape that existing `method_dispatch_pin.rs` cases use).
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Hash-stability pin extension for `show__T`
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ail/tests/mono_hash_stability.rs:34-41` — add 4 pin entries.
|
||||
- Modify: `examples/mono_hash_pin_smoke.ail.json` — extend to also exercise `show` calls so the same fixture drives both Eq/Ord and Show mono synthesis.
|
||||
|
||||
- [ ] **Step 1: Read `mono_hash_pin_smoke.ail.json` to identify the extension point.**
|
||||
|
||||
Run: `cat examples/mono_hash_pin_smoke.ail.json | head -80`
|
||||
|
||||
Expected: fixture has a `main` fn whose body exercises `eq 1 2`, `compare 1 2`, `eq true false`, etc. The implementer adds analogous `show 1`, `show true`, `show "x"`, `show 1.5` call sites in the same body (each wrapped in a let-binder so the heap-Str RC discipline holds).
|
||||
|
||||
- [ ] **Step 2: Extend the fixture with 4 new show call sites.**
|
||||
|
||||
Add four `show` call sites, each wrapped in `let s_show_<T> = show <literal> in ...` and chained into the existing body. The final body still exercises the existing Eq/Ord calls; the show calls live at the start or end of the chain.
|
||||
|
||||
If extending the existing fixture risks perturbing the Eq/Ord hash pins (it should not — body hashes are per-def, but the implementer cannot prove this in advance), the implementer creates a SECOND fixture `examples/show_hash_pin_smoke.ail.json` modelled on `mono_hash_pin_smoke.ail.json` and loads it from a second test fn in `mono_hash_stability.rs`. Implementer decides on first run; the load-bearing requirement is "show__T hashes get pinned somewhere".
|
||||
|
||||
- [ ] **Step 3: Extend `mono_hash_stability.rs:34-41`'s `pins` table.**
|
||||
|
||||
Add 4 new entries to the existing tuple table with placeholder hashes:
|
||||
|
||||
```rust
|
||||
let pins: &[(&str, &str)] = &[
|
||||
("eq__Int", "81d59ac38ab3663d"),
|
||||
("eq__Bool", "11da98a358b5979b"),
|
||||
("eq__Str", "277516bb7f195b2a"),
|
||||
("compare__Int", "d5d3f66b86c7e758"),
|
||||
("compare__Bool", "676e3ea0298a8795"),
|
||||
("compare__Str", "a532710899cf14fe"),
|
||||
("show__Int", "PLACEHOLDER_UPDATE_AFTER_FIRST_RUN"),
|
||||
("show__Bool", "PLACEHOLDER_UPDATE_AFTER_FIRST_RUN"),
|
||||
("show__Str", "PLACEHOLDER_UPDATE_AFTER_FIRST_RUN"),
|
||||
("show__Float", "PLACEHOLDER_UPDATE_AFTER_FIRST_RUN"),
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the test, capture actual hashes from the failure.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast -p ail --test mono_hash_stability 2>&1 | tail -30`
|
||||
|
||||
Expected: FAIL — at least one `show__T` assertion fails with a message like `show__Int body hash drifted` showing the actual hex.
|
||||
|
||||
If `show__T not found in prelude module`: the fixture extension at Step 2 did not produce the call sites needed to trigger mono synthesis. Re-verify Step 2.
|
||||
|
||||
If the test passes (no failure to capture): impossible — the placeholder strings cannot match real hashes. Re-check that the pin entries were inserted into the correct table.
|
||||
|
||||
- [ ] **Step 5: Update placeholders with the captured actual hashes.**
|
||||
|
||||
Replace each `PLACEHOLDER_UPDATE_AFTER_FIRST_RUN` with the actual 16-char hex from the failure output (one per show__T symbol).
|
||||
|
||||
- [ ] **Step 6: Re-run the test to confirm GREEN.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast -p ail --test mono_hash_stability 2>&1 | tail -5`
|
||||
|
||||
Expected: `test result: ok. 1 passed`.
|
||||
|
||||
---
|
||||
|
||||
## Task 7: DESIGN.md §"Prelude (built-in) classes" amendment
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/DESIGN.md:1888-1899` — amend the milestone-23 paragraph + append milestone-24 paragraph.
|
||||
|
||||
- [ ] **Step 1: Read DESIGN.md lines 1888-1899 to confirm the current text.**
|
||||
|
||||
Expected: lines 1888-1896 describe milestone 23's Eq/Ord shipping; line 1891-1892 contains the clause "`Show`, operator routing through `Eq`/`Ord`, and `print`-rewire remain out of scope per their original substantive reasons (heap-`Str` ABI, bench rebaseline)."
|
||||
|
||||
If the line range has shifted (unlikely but possible if another iter touched it), grep for the literal `"\`Show\`, operator routing"` and adjust.
|
||||
|
||||
- [ ] **Step 2: Amend the milestone-23 paragraph.**
|
||||
|
||||
Edit the clause at line 1891-1893: drop the "Show" word from the still-deferred list. The clause becomes:
|
||||
|
||||
```
|
||||
... `Show`, operator routing through `Eq`/`Ord`, and `print`-rewire
|
||||
remain out of scope per their original substantive reasons (heap-`Str`
|
||||
ABI, bench rebaseline). ...
|
||||
```
|
||||
|
||||
→ rewrites to →
|
||||
|
||||
```
|
||||
... Operator routing through `Eq`/`Ord` and `print`-rewire remain out
|
||||
of scope per their original substantive reasons (bench rebaseline,
|
||||
milestone-24 dependency on the post-mq dispatcher); `Show` itself
|
||||
ships in milestone 24.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Append milestone-24 paragraph.**
|
||||
|
||||
Immediately after the amended milestone-23 paragraph (before the "Primitive output goes through ..." sentence at line 1898), insert:
|
||||
|
||||
```
|
||||
Milestone 24 amends the above further: the prelude ships `class Show
|
||||
a where show : (a borrow) -> Str` and primitive `Show Int`,
|
||||
`Show Bool`, `Show Str`, `Show Float` instances. Float **is** included
|
||||
in Show (unlike Eq/Ord) — IEEE-754 makes structural equality and total
|
||||
ordering semantically dubious, but textual representation of a Float
|
||||
is well-defined modulo the NaN-spelling caveat in §"Float semantics".
|
||||
Each `Show <T>` instance body is a single-application lambda invoking
|
||||
the corresponding runtime primitive (`int_to_str`, `bool_to_str`,
|
||||
`str_clone`, `float_to_str`); no codegen intercept is required. The
|
||||
polymorphic helper `print : forall a. Show a => a -> () !IO` ships in
|
||||
the same milestone as the second iteration (24.3) and routes through
|
||||
`show` and `io/print_str`.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify DESIGN.md still parses as markdown.**
|
||||
|
||||
Run: `head -1920 docs/DESIGN.md | tail -50`
|
||||
|
||||
Expected: the section header `### Prelude (built-in) classes` is intact, both paragraphs are present, and the "Primitive output goes through..." follow-up sentence is unchanged.
|
||||
|
||||
If line count drifted: run `grep -c '^' docs/DESIGN.md` and verify the expected +10 (approximately) line growth.
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Integration verification + bench
|
||||
|
||||
**Files:** none modified; this task is verification-only.
|
||||
|
||||
- [ ] **Step 1: Full workspace test.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast 2>&1 | tail -5`
|
||||
|
||||
Expected: `test result: ok. <N> passed; 0 failed` where `<N>` is at least 548 (pre-iter baseline from mq.tidy) + 3 new tests (show_mono_synthesis, show_dispatch_pin × 2) = 551 or more.
|
||||
|
||||
If any tests fail, diagnose the specific failure and surface to Boss. Common potential failures:
|
||||
- Prelude hash-stability drift (milestone-23 hashes changed unexpectedly) — surface to Boss for hash-rebaseline decision.
|
||||
- mq3 fixture tests failing — implies the 22b migration accidentally touched an mq3 fixture; check Task 1 file list.
|
||||
- 22b assertion strings still reference old names — Task 2 Step 2's 7-step Edit sequence was incomplete; re-run with `replace_all`.
|
||||
|
||||
- [ ] **Step 2: Run mq1 fixtures to confirm xmod-Show stays green.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast 2>&1 | grep -E 'mq1.*xmod_constraint_class.*(ok|FAILED)' | head -5`
|
||||
|
||||
Expected: green. The pre-flight note confirms these fixtures have no `show` call sites, so adding `prelude.Show` should not cause ambiguity at any call site they exercise.
|
||||
|
||||
- [ ] **Step 3: Run mq3 fixtures to confirm intentional two-Show shapes still pass.**
|
||||
|
||||
Run: `cargo test --workspace --no-fail-fast -p ail --test mq3_multi_class_e2e 2>&1 | tail -10`
|
||||
|
||||
Expected: all mq3 multi-class E2E tests pass.
|
||||
|
||||
- [ ] **Step 4: Run bench scripts.**
|
||||
|
||||
Run: `bench/compile_check.py 2>&1 | tail -10`
|
||||
|
||||
Expected: exit 0; 24/24 stable (per mq.tidy baseline).
|
||||
|
||||
Run: `bench/cross_lang.py 2>&1 | tail -10`
|
||||
|
||||
Expected: exit 0; 25/25 stable.
|
||||
|
||||
Run: `bench/check.py 2>&1 | tail -15`
|
||||
|
||||
Expected: exit 0 or audit-ratifiable noise per the conservative-call convention. The `latency.implicit_at_rc.*` max-tail metric noise envelope (6 consecutive audits per mq.tidy journal) may produce 1-3 regressed metrics; conservative call is "baseline pristine, journal lineage continues" unless the noise envelope shifts character substantially.
|
||||
|
||||
If any bench script exits non-zero on a metric NOT in the documented noise envelope, surface to Boss for audit-ratification or rebaseline.
|
||||
|
||||
- [ ] **Step 5: Visual sanity-check the prelude file.**
|
||||
|
||||
Run: `python3 -c "import json; d=json.load(open('examples/prelude.ail.json')); print(f'{len(d[\"defs\"])} defs')"`
|
||||
|
||||
Expected output: `27 defs` (was 22 pre-iter + 5 new = 27).
|
||||
|
||||
- [ ] **Step 6: Write the per-iter journal.**
|
||||
|
||||
Path: `docs/journals/2026-05-13-iter-24.2.md`
|
||||
|
||||
Structure (matching the mq.tidy journal pattern):
|
||||
- `# iter 24.2 — Show class + 4 primitive instances + 22b migration`
|
||||
- `**Date:** 2026-05-13`
|
||||
- `**Started from:** d6d70bd`
|
||||
- `**Status:** DONE`
|
||||
- `**Tasks completed:** 8 of 8`
|
||||
- Summary paragraph
|
||||
- Per-task notes
|
||||
- Concerns section (the recon-Boss reconciliation, str_clone reachability verification, hash-stability hash-capture pattern)
|
||||
- Known debt (24.3 still pending; mq1 fixtures unmigrated by design)
|
||||
- Files touched (list)
|
||||
- Stats: `bench/orchestrator-stats/2026-05-13-iter-24.2.json`
|
||||
|
||||
Append the index line to `docs/journals/INDEX.md`:
|
||||
`- 2026-05-13 — iter 24.2: class Show + 4 primitive instances + 22b → TShow/tshow migration → 2026-05-13-iter-24.2.md`
|
||||
|
||||
---
|
||||
|
||||
## Self-review checklist (planner Step 5)
|
||||
|
||||
**Spec coverage:**
|
||||
- Architecture §1 (class Show + 4 instances) → Task 3 ✓
|
||||
- Architecture §2 (print fn) → NOT in 24.2 (iter 24.3 scope) ✓
|
||||
- Architecture §3 (dispatch routing) → Tasks 4, 5 (existence + singleton-pin) ✓
|
||||
- Architecture §4 (22b migration) → Tasks 1, 2 ✓
|
||||
- Testing §24.2 round-trip → Task 3 Step 5 ✓
|
||||
- Testing §24.2 mono synthesis → Task 4 ✓
|
||||
- Testing §24.2 dispatch pin → Task 5 ✓
|
||||
- Testing §24.2 hash stability → Task 6 ✓
|
||||
- Testing §24.2 22b regression → Task 2 ✓
|
||||
- Acceptance §1 (24.2 has landed) → Task 8 Step 1 ✓
|
||||
- Acceptance §3 (tests pass) → Task 8 Steps 1-3 ✓
|
||||
- Acceptance §4 (bench ratified) → Task 8 Step 4 ✓
|
||||
- Acceptance §5 (roadmap) → NOT in 24.2 (only flips at milestone close, in 24.3) ✓
|
||||
- DESIGN.md amendment → Task 7 ✓
|
||||
|
||||
**Placeholder scan:** the literal `PLACEHOLDER_UPDATE_AFTER_FIRST_RUN` in Task 6 Step 3 is part of a documented "capture-actual" pattern, not a plan placeholder — Task 6 Step 5 explicitly updates them with captured hashes. No genuine TBDs.
|
||||
|
||||
**Type consistency:** `Show`, `show`, `TShow`, `tshow`, `show__Int|Bool|Str|Float`, `int_to_str`, `bool_to_str`, `str_clone`, `float_to_str`, `prelude.Show`, `mq3_two_show_*`, `class_methods`, `method_to_candidate_classes` — all consistent across tasks.
|
||||
|
||||
**Step granularity:** every step is a single discrete action (Edit invocation, file read, single test run, single command). Task 1 Step 2 (rename across 21 files) is the longest at ~20 minutes if done sequentially, but each Edit is itself 30 seconds; the implementer pipelines.
|
||||
|
||||
**No commit steps:** none of the 8 tasks include `git commit`. The Boss commits at iter close per CLAUDE.md.
|
||||
Reference in New Issue
Block a user