# 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` (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` (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" ```