Skip to content

forge: check — weave the ADTs reachable through woven spec fns, and name harnesses after what they check (closes #92) - #95

Merged
dollspace-gay merged 2 commits into
mainfrom
fix/multi-adt-subprogram-adt-weave-92
Jul 29, 2026
Merged

forge: check — weave the ADTs reachable through woven spec fns, and name harnesses after what they check (closes #92)#95
dollspace-gay merged 2 commits into
mainfrom
fix/multi-adt-subprogram-adt-weave-92

Conversation

@dollspace-gay

@dollspace-gay dollspace-gay commented Jul 29, 2026

Copy link
Copy Markdown
Member

Closes #92. Two commits: the weave fix, and the diagnostics defect that made #92 look like two bugs.

1. Root cause — one defect, three surfaces

Not in build_vacuity_harness, and reachable_adt_deps / collect_item_adt_refs are both correct. The bug is the referrer seed in check_file_with_options:

let mut referrers: Vec<&Item> = vec![item];
referrers.extend(fn_deps.iter());
if matches!(item, Item::SpecFn(_)) { referrers.clear(); referrers.extend(item_spec_items.iter()); }
let adt_deps = reachable_adt_deps(&parsed.program, &referrers);

Every arm of item_subprogram weaves item_spec_items, but only the SpecFn arm seeded them as referrers. The referrer set was a strict subset of what the sub-program contained, so an ADT reachable only through a woven spec fn was never emitted. The pre-existing comment said so out loud: "The Item::Fn path is unchanged — its ADT referrers stay [item] + fn_deps exactly as before."

Item kind Referrers Symptom
Struct/Enum [item] — and collect_item_adt_refs is inert on ADT decls, so adt_deps was always empty a spec fn naming a different ADT dangles → E0425 → L0 → project assurance: FAILED
Fn [item] + fn_deps a spec fn taking an ADT the fn never names → vacuity harness won't elaborate → ForgeError aborts the run
SpecFn reachable spec-fn closure correct — which is why the single-ADT corpus never caught it

Why one ADT masked it: the only ADT is the checked item, which the ADT arm pushes itself. Same under-approximated-closure class proof-backends.md records for reachable_spec_fn_deps walking decl.body without decl.dec — and, as that note predicts, the pipeline fails closed (a Verus error, never a silent certification).

Fix: seed the referrers with everything woven — referrers.extend(item_spec_items) for every item kind, replaced rather than extended for Item::SpecFn (preserving #71's distinct per-spec-fn sub-program). The Struct/Enum arm drops the checked item from adt_deps before pushing it, so a spec fn naming that ADT cannot emit a duplicate definition (E0428).

2. The diagnostics defect that caused the misdiagnosis

run_verus took its scratch stem from program.items.first(). Since item_subprogram weaves ADT decls and spec fns ahead of the checked item, any item with something woven before it filed diagnostics under a sibling's name:

before:  [FAIL] postcondition not satisfied @ helper_check.rs:13:9
after:   [FAIL] postcondition not satisfied @ bad_check.rs:13:9

helper certifies L3 and had nothing to do with the failure — it was just woven first.

This is what made #92 read as two bugs. The reporter saw E0425 under is_owner_check.rs while is_owner certified L3, and concluded a harness that failed to compile could coexist with an L3 on the same item — that the refusal path had a hole. It does not. The failing harness belonged to Unused (correctly L0); only its name pointed at is_owner.

run_verus now takes the checked item's name as a subject parameter and no longer receives the sub-program at all — a harness that cannot see the woven set cannot name one of its members by accident. All seven call sites already had the subject in scope. The equivalence-probe site's single-item Program wrapper existed only to feed the old label machinery, so it and its f.clone() are gone.

REQ-2's no-. crate-stem property and Amendment item 6's forge_<stem>_<pid>_<n>/ scheme are unchanged — only the source of <stem> moved.

Corrections to the issue as filed

  1. "An unreferenced ADT grades L0" is a symptom, not a rule. An unreferenced ADT certifies L3 fine when no spec fn dangles. Unused was L0 only because its harness failed to compile — so there is no "spare declarations make files unshippable" policy question to settle. Claims 1 and 2 are the same bug.
  2. The refusal path has no hole — see §2 above.
  3. provenance_demo.th is not a bug. careless_query deliberately passes Tainted to a Sql sink — the negative case the demo exists to demonstrate. It has no .cert.json because it intentionally contains three must-fail functions; all six of its ADT items certify L3.

Verification

cargo test -p forge          807 passed, 2 failed*  (weave commit, full run)
new pins                     6 passed — all discriminating
conformance/multi_adt.th     pre-fix: Role/Action L0 E0425, project FAILED
                             post-fix: all five items L3
clippy -D warnings / fmt     PASS
doc-drift / req-status / req-registry / control-plane   PASS
verus 0.2026.05.24.ecee80a (the ci.yml pin)

* Both failures are pre-existing and environmental: strat_differential needs the Lean spine built locally (lake build Thermite.Strat.Cls.Wire). They fail identically on unmodified check.rs, which does not appear in that test file.

The pins are real pins, verified in both directions:

  • divergence_multi_adt_subprogram.rs — 3 of 4 fail pre-fix (two assertion failures plus the ForgeError abort). The 4th is the R-CHAR-3 oracle: adding a declaration nothing references cannot change what is provable about its siblings, so every shared item's level must equal the single-ADT twin's. It holds on both sides by construction, which is the point.
  • divergence_harness_names_checked_item.rs — fails pre-fix with got `helper_check.rs:13:9`. Its second test pins that helper genuinely certifies L3, because it was the pair of facts that fused into the phantom bug.

Corpus anchor: conformance/multi_adt.th, the first corpus program declaring more than one ADT, shaped as the Role×Action privilege lattice from the issue. Its golden pins authorize (following the fn-only golden convention — no existing golden pins an ADT item, and inventing that schema was out of scope); the ADT items' levels are pinned in the test.

Follow-up not taken here

map_kv.th breaks on verus 0.2026.06.14Set::new in the Map wrapper's generated spec_dom yields E0308: expected Set<int>, found Option<Set<int>>. Pre-existing, unrelated to this change, and invisible on the pinned 0.2026.05.24. Relevant whenever the verus pin gets bumped.

🤖 Generated with Claude Code

…#92)

No file declaring more than one ADT had ever certified. The per-item ADT weave
seeded `reachable_adt_deps` with `[item] + fn_deps`, while every arm of
`item_subprogram` weaves `item_spec_items` as well — the referrer set was a
strict subset of what the sub-program contained, so an ADT reachable only
through a woven spec fn was omitted from the emitted Verus and the run failed
closed on an unresolvable type.

Three surfaces of the one defect:

  1. a checked `struct`/`enum`: `collect_item_adt_refs` is inert on an ADT decl
     (field types are followed by the type-graph fixed point instead), so
     `adt_deps` came out empty for EVERY ADT item while the arm wove the file's
     whole `spec_items`. A spec fn naming a second ADT dangled -> L0 on E0425 ->
     `project assurance: FAILED`.
  2. a checked `fn` naming no ADT that a woven spec fn takes: the solver-vacuity
     harness failed to elaborate, which `vacuity_solver::interpret_summary`
     refuses as undetermined -> a `ForgeError` aborting the run.
  3. a checked `spec fn`: already correct, which is why the single-ADT corpus
     never exercised the gap.

A single ADT masks all of it: the only ADT is the checked item, which the ADT
arm pushes itself, so the decl is present by construction. Same
under-approximated-closure class as the `reachable_spec_fn_deps` body-only
omission recorded in proof-backends.md's increment-(i) note.

The seed now extends with `item_spec_items` for every item kind (replaced, not
extended, for `Item::SpecFn` — #71's distinct per-spec-fn sub-program), and the
ADT arm drops the checked item from `adt_deps` before pushing it so a spec fn
naming that ADT cannot produce a duplicate definition (E0428).

DESIGN SOURCES THIS ITERATION:
  - .design/forge/check.md (REQ-1 per-item pipeline, REQ-5 L3 iff 0 errors)
  - .design/basis/09-option-result.md, .design/basis/11-ergonomics.md,
    .design/basis/12-mutual-recursion.md, .design/lower/boundary-composition.md,
    .design/verified/proof-backends.md (the six docs routing check.rs)
  - thermite-design.md §5.3 (the isolated per-item sub-program), §6 (the ladder)
  - reference: conformance/sum.th, conformance/composition/cases.json

REQ STATUS:
  - REQ-1 (pipeline orchestration) SHIPPED — unchanged; this corrects its input.
  - REQ-5 (level determination) SHIPPED — unchanged; L3 iff verus reports 0
    errors. The ADT items were never un-provable, only un-emitted.
  R-HONEST-4: the correction is recorded as an Amendment in .design/forge/check.md.

VERIFICATION:
  cargo test -p forge: 807 passed, 2 failed — both pre-existing and
    environmental (strat_differential needs the Lean spine built locally;
    it fails identically on unmodified check.rs and does not reference it).
  new pin forge/tests/divergence_multi_adt_subprogram.rs: 4 passed. Confirmed a
    real pin — 3 of the 4 FAIL on pre-fix code (two assertion failures + the
    ForgeError abort); the 4th is the R-CHAR-3 oracle (a spare decl cannot
    change what is provable about its siblings, so every shared item's level
    must equal the single-ADT twin's) and holds on both sides by construction.
  conformance: multi_adt.th — pre-fix `Role`/`Action` both L0/E0425 and project
    FAILED; post-fix all five items L3, matching the hand-derived
    multi_adt.cert.json oracle subset.
  cargo clippy -p forge --all-targets -- -D warnings: PASS
  cargo fmt --all --check: PASS
  doc-drift: PASS (six check.rs-governing docs re-pinned)
  req-status / req-registry / control-plane: PASS
  verus 0.2026.05.24.ecee80a (the ci.yml pin)

Closes #92

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dollspace-gay dollspace-gay self-assigned this Jul 29, 2026
The diagnostics half of #92. `run_verus` took its scratch-dir/`.rs` stem from
`program.items.first()`, and `item_subprogram` weaves ADT decls and spec fns
ahead of the checked item (which it pushes last), so any item with something
woven before it filed its diagnostics under a sibling's name:

  spec fn helper(n: u64) -> u64 { n }
  fn bad(x: u64) -> u64 ens result == helper(x) + 1 { x }   // cannot hold

  before:  [FAIL] postcondition not satisfied @ helper_check.rs:13:9
  after:   [FAIL] postcondition not satisfied @ bad_check.rs:13:9

`helper` certifies L3 and has nothing to do with the failure.

This is not cosmetic: it is what made #92 read as two defects. The reporter saw
E0425 under `/tmp/forge_is_owner_check_*/is_owner_check.rs` while `is_owner`
certified L3, and concluded a harness that failed to compile could coexist with
an L3 on the same item — that the L3 refusal path had a hole. It does not. The
failing harness belonged to `Unused` (correctly L0); only its NAME pointed at
`is_owner`. REQ-4's "the failed obligation and its source position" is not
satisfied by a position under another item's filename.

`run_verus` now takes the checked item's name as a `subject` parameter and no
longer receives the sub-program at all — a harness that cannot see the woven set
cannot name one of its members by accident. All seven call sites already had the
subject in scope (it is what they pass to `assemble_certificate`). The
equivalence-probe site's single-item `Program` wrapper existed only to feed the
old label machinery, so it and its `f.clone()` are removed.

Amendment item 6's `<stem>.rs`-inside-`forge_<stem>_<pid>_<n>/` scheme and the
AC-4 no-`.` crate-stem property are unchanged — only the source of `<stem>` moved.

DESIGN SOURCES THIS ITERATION:
  - .design/forge/check.md (REQ-2 scratch/crate-stem, REQ-4 per-obligation
    position, AC-4 crate-name gotcha, Amendment item 6 scratch naming)
  - reference: conformance/sum.th

REQ STATUS:
  - REQ-2 (verus invocation, temp file, crate-name gotcha) SHIPPED — the no-`.`
    stem property is unchanged; only the stem's source moved.
  - REQ-4 (per-obligation results + counterexamples) SHIPPED — a failing
    obligation's position now identifies its own subject.
  R-HONEST-4: recorded in .design/forge/check.md's #92 Amendment.

VERIFICATION:
  new pin forge/tests/divergence_harness_names_checked_item.rs: 2 passed.
    Confirmed discriminating — pre-fix it FAILS with
    "got `helper_check.rs:13:9`"; post-fix it passes.
  cargo clippy -p forge --all-targets -- -D warnings: PASS
  cargo fmt --all --check: PASS
  doc-drift: PASS (six check.rs-governing docs re-pinned)
  req-status / req-registry: PASS
  cargo test -p forge: in progress at commit time (39/95 binaries, 0 failures);
    the preceding commit's full run was 807 passed / 2 pre-existing
    environmental failures (strat_differential needs the Lean spine built).
  verus 0.2026.05.24.ecee80a (the ci.yml pin)

Refs #92

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dollspace-gay dollspace-gay changed the title forge: check — weave the ADTs reachable through woven spec fns (closes #92) forge: check — weave the ADTs reachable through woven spec fns, and name harnesses after what they check (closes #92) Jul 29, 2026
@dollspace-gay
dollspace-gay merged commit d517f90 into main Jul 29, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forge check: no file with more than one ADT has ever certified — the vacuity harness omits the woven decls, and an unreferenced decl grades L0

1 participant