Skip to content

Unused-code elimination: napl gc (language feature) #33

Description

@omridevk

Context

Maintainer directive: removing unused code MUST be part of the language design. Prompts are the source of truth, so generated code that no current prompt demands is drift wearing a green test suite. Incremental gens edit toward the new prompt but nothing today guarantees they remove what only the OLD prompt demanded; the gen gate is cargo test, and rustc's dead_code output is a warning the gate ignores.

Survey (2026-07-25): module-level state is CLEAN (every entry in ir/, prompts-at-gen/, attribution/, mapl/, and every workspace member crate maps 1:1 to a current .napl prompt; zero orphans). The exposure is intra-file: items, branches, helpers, and imports inside locked generated files that survived prompt revisions which no longer demand them.

Spec of record

docs/specs/2026-07-25-unused-code-gc-plan.md, rev 5, status CONVERGED. It converged through five codex rounds (rev 1 RETHINK, rev 2 RETHINK, rev 3 RETHINK, rev 4 RETHINK, rev 5 SOUND-WITH-FIXES applied). Committed at 1cb6e4b.

Architecture: four facts kept separate

Deadness is never treated as a single executable signal. The design distinguishes four facts:

  1. Compiler fact: rustc diagnosed the item unused under the declared target matrix.
  2. Graph fact: the module is unreachable from declared roots in the prompt DAG, or a declared dep edge is unused.
  3. Provenance observation: a deterministic structural verdict from attribution and journal history (SUPPORTED / CONTRADICTED / UNKNOWN), evidence, never proof.
  4. Policy decision: deletion is authorized only when, jointly, there is a deletion-eligible compiler diagnostic (dead_code or unused_imports ONLY), private-item clearance, and tier-3 SUPPORTED (not merely non-CONTRADICTED; UNKNOWN is never eligible).

Tiers

  • Tier 1 (compiler hygiene, gate-enforced): workspace-wide deny lints (dead_code, unused_imports, unused_variables, unreachable_code, unused_must_use), of which only dead_code and unused_imports are deletion-eligible (the other three are gate-only hygiene, remediated by normal prompt-driven gens). The gate gains cargo check --workspace --all-targets. Rollout is one global flip in 4 reviewed steps: (1) manifest-renderer prompts gain [workspace.lints] at warn plus member [lints] workspace = true, golden-sanctioned; (2) inventory every warning across the matrix with zero code changes, reporting the coverage denominator (modules with api: declared over total); (3) remediate via removal gens until the inventory is empty; (4) flip to deny in one renderer gen, gated on BOTH an empty inventory AND api: retrofit completion across all modules. Generated crates prohibit (gate-checked) feature-gated public surface, bins/examples/benches/build scripts, glob reexports, feature-only deps, and doctests, keeping every later analysis decidable.
  • Tier 2 (prompt-graph facts, no compilation): module reachability (report-only) plus unused dep-edge detection from deps: frontmatter. A declared dep is UNUSED only when no path reference to its crate identifier appears in any .rs file of the crate (lib plus tests), with normative package-to-identifier resolution; unresolvable macro references report UNKNOWN; a scratch compile-after-removal safety check demotes to UNKNOWN on failure. The fix is a prompt edit deleting the dep plus a regen.
  • Tier 3 (provenance, deterministic structural rule): item correspondence traced by AST identity through journal patch lineage, then classified CONTRADICTED (current attribution still maps a prompt range onto the item, deletion refused, beats everything), SUPPORTED (no current attribution mapping AND a unique lineage transition where demand was removed while the item survived), or UNKNOWN (everything else, blocks --fix). No model judgment runs inside the verb. The store rewrite upgrades reconstruction from partial journal replay to exact content-addressed lookups; the evidence label stays either way.

State-hygiene orphan sweep

Pre-store (current layout): REPORT-ONLY. napl gc diffs mapl/, ir/, attribution/, prompts-at-gen/, and member crate dirs against the prompt set and reports orphans, deleting nothing (today's journal cannot represent deletion; survey shows zero orphans). Post-store: the sweep governs only current derived and materialized views, using the store's real guarantee (one authoritative snapshot transition carrying the deletion event, then recoverable monotonic installation, with crash fixtures at each boundary). Immutable historical objects are never orphan candidates.

Removal is toolchain-constructed, not agent-generated

There is no coding agent in the removal loop. The toolchain builds the result counterfactually: take the current tree, delete exactly the enumerated cleared item spans, delete imports proven unused under that counterfactual, run the pinned formatter, and transform the attribution document under a normative rule (mapping wholly inside a deleted span is dropped and recorded in the gc event; a mapping partially intersecting a deleted span ABORTS; surviving mappings relocate by line-offset then formatter-diff projection, and any non-one-to-one projection aborts). The constructed tree IS the removal; the output is byte-determined by the inputs, so there is nothing to validate against a claim. The full gate battery (tests, conformance, deny lints) must pass on the constructed tree or the removal aborts with nothing written. Multi-module runs are compensating forward sequences, each independently green-gated, not batches.

Prerequisite: the api: frontmatter field

Foundational for tiers 1 and 2. Frontmatter gains an optional api: list of canonical export identities with a two-mode coverage grammar (leaf <namespace> <path> with structural-containment-only coverage, and subtree mod <path>). Strict equality both ways at gen time over the covered surface. Modules without api: keep today's behavior and count against the coverage denominator until retrofitted. This is its own slice.

Dependencies

  • Blocks on store rewrite (Store rewrite campaign #30): the api: frontmatter feature and the tier-1 rollout both land after store Phase A completes (the store campaign keeps regenerating the workspace manifest and must not race a renderer change).
  • napl gc --fix blocks on the store rewrite's changeset-compiler phase, which receives these named requirements: a gc gen kind, deletion-capable file events (a path with no after-state), map ownership transitions for deleted files, and the counterfactual constructor (with the normative attribution transformation). No batch-transaction requirement is handed over.
  • napl gc analysis verb (tiers 1 through 3 reporting, report-only pre-store) can land as its own toolchain module lane once tier-1 groundwork exists.

Acceptance criteria

  • api: frontmatter feature (schema plus strict validation) lands after store Phase A.
  • Tier-1 rollout steps 1 through 4 land after store Phase A; step 4 additionally gated on api: retrofit completion.
  • napl gc analysis verb reports tier-1 diagnostics (deletion-eligible vs gate-only), tier-2 module reachability and dep-edge findings, tier-3 verdicts, and state-tree orphans; deterministic given the same workspace; report-only pre-store.
  • napl gc --fix (item removal plus post-store orphan-view transactions) lands after the changeset-compiler phase, with removal toolchain-constructed and byte-determined (no agent).
  • The verification positive detections and negative battery from the spec pass, including constructor determinism (byte-identical constructed tree on repeat) and the post-store crash fixtures.

Open questions (maintainer)

  1. Retrofit sweep granularity: one module per gen (recommended, reviewable) or batched?
  2. Should tier-2 module-unreachability ever gate CI, or stay report-only (spec recommends report-only until symbol-level deps exist)?

References

  • docs/specs/2026-07-25-unused-code-gc-plan.md (rev 5, CONVERGED, commit 1cb6e4b)
  • docs/specs/2026-07-25-journal-store-design.md (store-rewrite provenance upgrade and changeset-compiler phase)
  • Store rewrite campaign tracking issue: Store rewrite campaign #30

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions