# 22-tidy primitive-name-set consolidation — Implementation Plan

> **Parent spec:** `docs/specs/0002-22-typeclasses.md` (milestone 22, closed 2026-05-09)
>
> **Audit anchor:** `docs/JOURNAL.md` 2026-05-09 entry "Iteration 22-tidy: DESIGN.md and spec drift after milestone-22 close" → "Carried debt" → primitive-name-set consolidation.
>
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
> to run this plan. Steps use `- [ ]` checkboxes for tracking.

**Goal:** retire the four duplicate `matches!(name.as_str(), "Int" | "Bool" | "Str" | "Unit")` sites by introducing one shared predicate `ailang_core::primitives::is_primitive_name(&str) -> bool`, and wire `mono::primitive_surface_name` through the same predicate so the primitive-name set has a single source of truth.

**Architecture:** a small module `crates/ailang-core/src/primitives.rs` exporting one predicate function plus one surface-name helper for callers that need the static-lifetime literal. Both consumer crates (`ailang-check`, `ailang-codegen`) already depend on `ailang-core`, so cross-crate consolidation is free. No behaviour change — every call site lands at the same boolean answer it does today.

**Tech Stack:** `crates/ailang-core` (new module), `crates/ailang-check/src/{lib.rs, linearity.rs, mono.rs}`, `crates/ailang-codegen/src/subst.rs`.

**Files this plan creates or modifies:**

- Create: `crates/ailang-core/src/primitives.rs` — single home for the primitive-name set; exports `pub fn is_primitive_name(&str) -> bool` and `pub fn primitive_surface_name(&str) -> Option<&'static str>`.
- Modify: `crates/ailang-core/src/lib.rs` — add `pub mod primitives;` declaration.
- Modify: `crates/ailang-check/src/linearity.rs:143` — replace inline `matches!` with helper call.
- Modify: `crates/ailang-check/src/lib.rs:1279` — replace inline `matches!` with helper call.
- Modify: `crates/ailang-check/src/lib.rs:2261` — replace inline `matches!` with helper call.
- Modify: `crates/ailang-codegen/src/subst.rs:169` — replace inline `matches!` with helper call.
- Modify: `crates/ailang-check/src/mono.rs:316-327` — `primitive_surface_name` delegates to the shared `primitive_surface_name`.
- Modify: `docs/JOURNAL.md` — append iteration entry.

---

## Task 1: introduce the helper module in `ailang-core`

Single home for the primitive-name predicate and the surface-name mapping.

**Files:**
- Create: `crates/ailang-core/src/primitives.rs`
- Modify: `crates/ailang-core/src/lib.rs` (one-line `pub mod primitives;`)

- [ ] **Step 1: write `crates/ailang-core/src/primitives.rs`**

```rust
//! The primitive-name set: `Int`, `Bool`, `Str`, `Unit`. Single
//! source of truth for "is this name a built-in zero-arity type
//! constructor?" Every typecheck and codegen site that gates
//! behaviour on the primitive set goes through this module.
//!
//! Adding a primitive: append to BOTH functions below; the four
//! consumers compile-fail-loudly if the predicate is forgotten
//! (the failure is a missed branch, not a panic), but
//! `primitive_surface_name` will silently return `None` for the
//! new name unless its `match` is extended too. Keep the two
//! functions in lockstep.

/// Returns `true` iff `name` is one of the built-in zero-arity
/// primitive type constructors. Cross-crate predicate consumed by
/// `ailang-check` (`linearity::is_heap_type`, `lib::check_type_well_formed`,
/// `lib::qualify_local_types`) and `ailang-codegen`
/// (`subst::qualify_local_types_codegen`).
pub fn is_primitive_name(name: &str) -> bool {
    matches!(name, "Int" | "Bool" | "Str" | "Unit")
}

/// Returns the static-lifetime surface name iff `name` is a
/// primitive. Used by `mono::primitive_surface_name` to embed the
/// human-readable form in monomorphised symbol names; the static
/// lifetime is what makes the symbol-builder's `&'static str`
/// signature work.
pub fn primitive_surface_name(name: &str) -> Option<&'static str> {
    match name {
        "Int" => Some("Int"),
        "Bool" => Some("Bool"),
        "Str" => Some("Str"),
        "Unit" => Some("Unit"),
        _ => None,
    }
}
```

- [ ] **Step 2: register the module in `ailang-core::lib.rs`**

Locate the `pub mod` block in `crates/ailang-core/src/lib.rs` (currently lines 57-62: `ast`, `canonical`, `desugar`, `hash`, `pretty`, `workspace`). Insert `pub mod primitives;` in alphabetical position between `pretty` and `workspace`:

```rust
pub mod ast;
pub mod canonical;
pub mod desugar;
pub mod hash;
pub mod pretty;
pub mod primitives;
pub mod workspace;
```

- [ ] **Step 3: build clean**

Run: `cargo build --workspace 2>&1 | tail -5`
Expected: builds clean. New module compiles; no dead-code warning (next task wires consumers).

- [ ] **Step 4: commit**

```bash
git add crates/ailang-core/src/primitives.rs crates/ailang-core/src/lib.rs
git commit -m "iter 22-tidy.4: ailang-core::primitives — single home for the primitive-name set"
```

---

## Task 2: wire the four `matches!` consumer sites through the helper

Replaces every duplicate `matches!(name.as_str(), "Int" | "Bool" | "Str" | "Unit")` (or order-equivalent) with `ailang_core::primitives::is_primitive_name(name)`. The four sites are independent — bundling them in one task because the change is mechanical and any single-site commit would leave a dead-code warning on `is_primitive_name` until the rest land.

**Files:**
- Modify: `crates/ailang-check/src/linearity.rs:143`
- Modify: `crates/ailang-check/src/lib.rs:1279`
- Modify: `crates/ailang-check/src/lib.rs:2261`
- Modify: `crates/ailang-codegen/src/subst.rs:169`

- [ ] **Step 1: replace `linearity.rs:143`**

In `crates/ailang-check/src/linearity.rs`, change:

```rust
fn is_heap_type(t: &Type) -> bool {
    match t {
        Type::Con { name, .. } => !matches!(name.as_str(), "Int" | "Bool" | "Str" | "Unit"),
```

to:

```rust
fn is_heap_type(t: &Type) -> bool {
    match t {
        Type::Con { name, .. } => !ailang_core::primitives::is_primitive_name(name),
```

- [ ] **Step 2: replace `lib.rs:1279`**

In `crates/ailang-check/src/lib.rs:1279` (inside `check_type_well_formed`), change:

```rust
            let is_primitive = matches!(name.as_str(), "Int" | "Bool" | "Unit" | "Str");
```

to:

```rust
            let is_primitive = ailang_core::primitives::is_primitive_name(name);
```

- [ ] **Step 3: replace `lib.rs:2261`**

In `crates/ailang-check/src/lib.rs:2261` (inside `qualify_local_types`), change:

```rust
            } else if matches!(name.as_str(), "Int" | "Bool" | "Unit" | "Str") {
```

to:

```rust
            } else if ailang_core::primitives::is_primitive_name(name) {
```

- [ ] **Step 4: replace `subst.rs:169`**

In `crates/ailang-codegen/src/subst.rs:169` (inside `qualify_local_types_codegen`), change:

```rust
            } else if matches!(name.as_str(), "Int" | "Bool" | "Unit" | "Str") {
```

to:

```rust
            } else if ailang_core::primitives::is_primitive_name(name) {
```

- [ ] **Step 5: build clean**

Run: `cargo build --workspace 2>&1 | tail -5`
Expected: builds clean. The `is_primitive_name` helper now has four consumers; no dead-code warning.

- [ ] **Step 6: workspace test sweep**

Run: `cargo test --workspace 2>&1 | grep -E "^(test result|FAILED)" | tail -25`
Expected: every `test result: ok.` line; no `FAILED`. Total test count unchanged at 345.

- [ ] **Step 7: commit**

```bash
git add crates/ailang-check/src/linearity.rs crates/ailang-check/src/lib.rs crates/ailang-codegen/src/subst.rs
git commit -m "iter 22-tidy.4: route 4 matches! sites through is_primitive_name"
```

---

## Task 3: route `mono::primitive_surface_name` through the shared helper

`mono.rs:316-327` is structurally different — it returns `Option<&'static str>` and pattern-matches on a `Type::Con { name, args } if args.is_empty()` to gate on zero-arity. Keep that gating; delegate the inner mapping to `ailang_core::primitives::primitive_surface_name`.

**Files:**
- Modify: `crates/ailang-check/src/mono.rs:316-327`

- [ ] **Step 1: replace the body**

In `crates/ailang-check/src/mono.rs`, replace:

```rust
/// Iter 22b.3: returns the surface name iff `ty` is a zero-arity
/// primitive `Type::Con`. Used by [`mono_symbol`] to gate the
/// human-readable form. The match is intentionally narrow:
/// `Int<args>` (which is malformed but parser-accepting) is
/// treated as compound, so it falls to the hash form.
fn primitive_surface_name(ty: &Type) -> Option<&'static str> {
    match ty {
        Type::Con { name, args } if args.is_empty() => match name.as_str() {
            "Int" => Some("Int"),
            "Bool" => Some("Bool"),
            "Str" => Some("Str"),
            "Unit" => Some("Unit"),
            _ => None,
        },
        _ => None,
    }
}
```

with:

```rust
/// Returns the surface name iff `ty` is a zero-arity primitive
/// `Type::Con`. Used by [`mono_symbol`] to gate the human-readable
/// form. The match is intentionally narrow: `Int<args>` (malformed
/// but parser-accepting) is treated as compound, so it falls to
/// the hash form. The primitive-set itself lives in
/// [`ailang_core::primitives::primitive_surface_name`].
fn primitive_surface_name(ty: &Type) -> Option<&'static str> {
    match ty {
        Type::Con { name, args } if args.is_empty() => {
            ailang_core::primitives::primitive_surface_name(name)
        }
        _ => None,
    }
}
```

- [ ] **Step 2: build clean**

Run: `cargo build --workspace 2>&1 | tail -5`
Expected: builds clean.

- [ ] **Step 3: workspace test sweep + bench gates 0/0/0 + targeted regressions**

Run: `cargo test --workspace 2>&1 | grep -E "^(test result|FAILED)" | tail -25`
Expected: every `test result: ok.` line; no `FAILED`. 345 tests.

Run: `cargo test --workspace --test typeclass_22c 2>&1 | tail -5`
Expected: `user_class_instance_over_user_adt_builds_and_runs ... ok` (this test runs the mono pass, exercising `primitive_surface_name` through real fixtures).

- [ ] **Step 4: commit**

```bash
git add crates/ailang-check/src/mono.rs
git commit -m "iter 22-tidy.4: mono::primitive_surface_name delegates to ailang-core"
```

---

## Task 4: verify + JOURNAL entry

**Files:**
- Modify: `docs/JOURNAL.md`

- [ ] **Step 1: full workspace test sweep**

Run: `cargo test --workspace 2>&1 | grep -E "^test result" | wc -l`
Expected: ≥ 23 (count of test-binary `test result:` lines; the workspace already had 23 such lines in the env-construction unify close).

Run: `cargo test --workspace 2>&1 | grep -c "FAILED"`
Expected: `0`.

- [ ] **Step 2: bench gates**

Run: `python3 bench/check.py 2>&1 | tail -3`
Expected: `0 regressed` in the summary line.

Run: `python3 bench/compile_check.py 2>&1 | tail -3`
Expected: `0 regressed`.

Run: `python3 bench/cross_lang.py 2>&1 | tail -3`
Expected: `0 regressed`.

- [ ] **Step 3: confirm zero remaining duplicates**

Run: `grep -rn 'matches!(name.as_str(), "Int" | "Bool"\|matches!(name.as_str(), "Bool" | "Int"\|matches!(name, "Int" | "Bool"' crates/ --include="*.rs" 2>&1 | grep -v primitives.rs | head -10`
Expected: empty (all four duplicate `matches!` sites have been routed; only `crates/ailang-core/src/primitives.rs` retains the literal set).

- [ ] **Step 4: append JOURNAL entry**

Append to `docs/JOURNAL.md`:

```markdown
## 2026-05-10 — Iteration 22-tidy.4: primitive-name-set consolidation

Closing the milestone-22 carried-debt item flagged in the 22b.3
JOURNAL and re-flagged at milestone-22 audit close: the
primitive-name set `{Int, Bool, Str, Unit}` was duplicated across
five sites (`crates/ailang-codegen/src/subst.rs:169`,
`crates/ailang-check/src/linearity.rs:143`,
`crates/ailang-check/src/lib.rs:1279` and `:2261`,
`crates/ailang-check/src/mono.rs:316-327`).

This iteration introduces `crates/ailang-core/src/primitives.rs`
exporting `is_primitive_name(&str) -> bool` plus
`primitive_surface_name(&str) -> Option<&'static str>`. The four
`matches!` consumer sites now route through the predicate; the
mono-pass surface-name helper retains its outer zero-arity gating
and delegates the inner mapping to the same module.

Tasks (commit subjects):

- 22-tidy.4: `ailang-core::primitives` — single home for the
  primitive-name set
- 22-tidy.4: route 4 matches! sites through is_primitive_name
- 22-tidy.4: mono::primitive_surface_name delegates to ailang-core

Acceptance: 4 `matches!` sites + 1 mono.rs site routed; full
workspace test sweep 345 green; bench gates 0/0/0; grep confirms
no remaining duplicate predicate. The audit-flagged milestone-22
carried-debt item closes; the remaining carried items
(parse-fn/data/const strict duplicate-clause detection, Form-B
prose printer arms for ClassDef/InstanceDef, defensive lib.rs
gap-related sites) stay queued.
```

- [ ] **Step 5: commit**

```bash
git add docs/JOURNAL.md
git commit -m "iter 22-tidy.4: journal entry"
```
