Files
AILang/docs/plans/0008-22-tidy-primitive-names.md
T
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

12 KiB

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-327primitive_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

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

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

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

to:

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:

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

to:

            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:

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

to:

            } 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:

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

to:

            } 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
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:

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

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

## 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
git add docs/JOURNAL.md
git commit -m "iter 22-tidy.4: journal entry"