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.
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.md2026-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/implementto 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; exportspub fn is_primitive_name(&str) -> boolandpub fn primitive_surface_name(&str) -> Option<&'static str>. - Modify:
crates/ailang-core/src/lib.rs— addpub mod primitives;declaration. - Modify:
crates/ailang-check/src/linearity.rs:143— replace inlinematches!with helper call. - Modify:
crates/ailang-check/src/lib.rs:1279— replace inlinematches!with helper call. - Modify:
crates/ailang-check/src/lib.rs:2261— replace inlinematches!with helper call. - Modify:
crates/ailang-codegen/src/subst.rs:169— replace inlinematches!with helper call. - Modify:
crates/ailang-check/src/mono.rs:316-327—primitive_surface_namedelegates to the sharedprimitive_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-linepub 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"