Files
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00

37 KiB
Raw Permalink Blame History

Iter 24.2 — class Show + 4 instances + 22b migration — Implementation Plan

Parent spec: docs/specs/0024-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 Showclass TShow (in rustdoc)
  • show xtshow 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 Showclass TShow (replace_all)
  7. Edit: show xtshow 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:

    {
      "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:

    {
      "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:

    {
      "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.

{
  "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.
//! 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:

//! 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:

    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.