diff --git a/docs/plans/2026-05-10-22-tidy-primitive-names.md b/docs/plans/2026-05-10-22-tidy-primitive-names.md new file mode 100644 index 0000000..e7ef966 --- /dev/null +++ b/docs/plans/2026-05-10-22-tidy-primitive-names.md @@ -0,0 +1,338 @@ +# 22-tidy primitive-name-set consolidation — Implementation Plan + +> **Parent spec:** `docs/specs/2026-05-09-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" +```