Skip to content

fix(804): assert/assume facts discharge later obligations (v0.0.179)#805

Merged
aallan merged 5 commits into
mainfrom
fix/804-assert-assume-facts
Jun 26, 2026
Merged

fix(804): assert/assume facts discharge later obligations (v0.0.179)#805
aallan merged 5 commits into
mainfrom
fix/804-assert-assume-facts

Conversation

@aallan

@aallan aallan commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the assume-half of the weakest-precondition rule for assert/assume (spec §6.4.1) — the follow-up to #800, which added only the prove-half. Closes #804 (batch 2 of the #392 soundness audit), and cuts release v0.0.179, folding in the batch-1 fixes (#799/#800/#801) that have been riding [Unreleased] since #803.

What changed

A body assert(P) / assume(P) now adds P to the assumption context for subsequent obligations:

P reaches every later obligation in the statement's block, and a top-level assert/assume also reaches the postcondition and a refined return.

Why it's sound — a completeness gain, not a soundness risk

The §11.14.1 runtime trap guarantees execution only proceeds past assert(P) in worlds where P holds — so P is assumable downstream even when the assert itself only reached Tier 3. This moves obligations Tier 3 → Tier 1 and removes spurious E503 / E500 / E505 errors where a prior assert provably guards the site:

-- was a false E503: Z3's negative witness is unreachable (the assert traps first)
{ assert(@Int.0 >= 0); let @Nat = @Int.0; ... }

Two soundness invariants are pinned by guard tests (green both pre- and post-fix; a buggy implementation reddens them):

  • forward-only — a later assert never discharges an earlier obligation;
  • branch-local — a branch assert never leaks past its if/match.

Implementation

  • _assumed_block_fact — shared helper returning a bare assert/assume statement's translated predicate.
  • primitive-op walk: pushes the fact onto smt._path_conditions (snapshot + truncate per block — the scoped channel check_valid already folds into every obligation).
  • nat-binding walk: appends to its per-block block_assumptions copy (auto-scoped, auto-discarded at block exit).
  • _verify_fn: collects unconditional top-level facts and spans them across the post-body checks (ensures + refined-return).

Tests (test-first)

tests/test_soundness_392.py grows to 27 tests (8 new in TestAssertAssumeFacts804). Each behavior test was confirmed RED on the pre-fix verifier — stashing only vera/verifier.py flips 6 behavior tests red while the 2 soundness guards stay green. Extended the ch06_assert_assume conformance program with a discharge demonstration (vera run → 30).

Validation

  • Full suite: 4898 passed, 16 skipped, 0 failed.
  • mypy clean; ruff + ruff --select S clean; conformance 92/92; walker-coverage OK; doc-counts / version-sync / site-assets green.

Closes #804. Refs #392, #800.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved how assert(P) and assume(P) contribute “assume-half” facts to later verification, including correct Tier-1 discharge, postcondition and call precondition handling, and locality within if/match.
    • Added/adjusted soundness checks for division-by-zero obligations and clarified diagnostics for weaker assertions.
  • Tests

    • Added extensive regression coverage for #804 assume-half behaviour, updated the related soundness test, and refreshed soundness metrics.
  • Documentation

    • Updated CHANGELOG, HISTORY, README, ROADMAP, and TESTING for version 0.0.179.

…f of #800)

The assume-half of the WP rule (spec section 6.4.1): a body assert(P)/assume(P)
now adds P to the assumption context for subsequent obligations in its block,
and a top-level assert/assume to the postcondition and refined return. #800
added only the prove-half; this completes the rule.

Moves obligations Tier 3 -> Tier 1 and removes false E503/E500/E505 where a
prior assert provably guards the site (the runtime trap makes the negative
witness unreachable). Sound by construction: facts flow forward-only and stay
branch-local, both pinned by regression guards.

Implementation: a shared _assumed_block_fact helper; the primitive-op walk
pushes the fact onto smt._path_conditions (snapshot/truncate per block), the
nat-binding walk appends to its block_assumptions copy, and _verify_fn collects
top-level facts for the post-body (ensures / refined-return) checks.

Releases v0.0.179, folding in the #392 audit batch 1 (#799/#800/#801) that rode
[Unreleased]. test_soundness_392.py grows to 27 tests (8 new, RED-first
confirmed), an extended ch06_assert_assume conformance program, full suite green
(4898 passed).

Closes #804. Refs #392, #800.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.14815% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.93%. Comparing base (1c577f0) to head (45f7a83).

Files with missing lines Patch % Lines
vera/verifier.py 97.22% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #805   +/-   ##
=======================================
  Coverage   91.92%   91.93%           
=======================================
  Files          89       89           
  Lines       26615    26658   +43     
  Branches      321      321           
=======================================
+ Hits        24467    24509   +42     
- Misses       2140     2141    +1     
  Partials        8        8           
Flag Coverage Δ
javascript 65.33% <ø> (ø)
python 95.00% <98.14%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR bumps the project to v0.0.179 and adds verifier handling for bare assert(P)/assume(P) facts so they can discharge later obligations, with updated regression coverage, conformance metadata, and release notes.

Changes

Assert/assume fact discharge

Layer / File(s) Summary
Top-level fact collection
vera/verifier.py
Adds helpers for bare assert(P) and assume(P) statements and threads unconditional top-level facts through _verify_fn for post-body obligations before removing them for decreases checks.
Block-scoped fact propagation
vera/smt.py, vera/verifier.py
Updates block translation and obligation walking so later siblings see translated bare assert/assume facts, with path-condition state restored when the block exits.
Regression coverage and conformance
tests/test_soundness_392.py, tests/conformance/manifest.json
Extends the soundness regressions with Batch 2 assert/assume cases, replaces the earlier accumulation test, and widens the ch06_assert_assume conformance metadata.
Release and status metadata
CHANGELOG.md, HISTORY.md, README.md, ROADMAP.md, TESTING.md, pyproject.toml, vera/__init__.py
Bumps release/version metadata to v0.0.179 across the changelog, history, README, roadmap, testing totals, package version, and module version.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • aallan/vera#778 — Also affects verifier discharge of div_zero-style obligations through assert-driven path conditions.

Suggested labels

compiler, tests, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main fix: assert/assume facts now discharge later obligations in v0.0.179.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/804-assert-assume-facts

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 218: The project-status counts are out of sync between the README and
HISTORY, so update the release figure in the status blurb or the corresponding
count in HISTORY.md to match. Use the README.md active-development summary and
the HISTORY.md tagged-releases section as the source of truth, and make sure the
release count is consistent across both places after the edit.

In `@tests/test_soundness_392.py`:
- Around line 393-425: Add mirrored top-level assume coverage for both discharge
paths in test_top_level_assert_discharges_postcondition and
test_top_level_assert_discharges_refined_return. Update the existing
_verify-based cases to include equivalent assume(`@Int.0` > 5) and assume(`@Int.0` >
0) scenarios so the same ensures and refine_bind checks verify that top-level
assume facts are threaded correctly by _verify_fn. Keep the assert tests, but
add the assume variants in the same test module to catch regressions where
assumptions are dropped before postcondition or refined-return validation.

In `@vera/verifier.py`:
- Around line 1446-1458: Top-level assert/assume facts are only being appended
after `translate_expr(decl.body)` has already translated sibling call sites, so
call-precondition checks can miss those path conditions and emit spurious
E501/Tier 3 results. Update `SmtContext._translate_block` (or add an equivalent
verifier-side call-precondition pass) so
`_collect_top_level_assert_facts`-derived facts are threaded positionally into
call-precondition translation before later sibling calls are checked, matching
the existing block-walker behavior and the `_path_conditions` handling around
`check_valid`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 03e0c87f-392e-492a-bd0b-47d2dbce6897

📥 Commits

Reviewing files that changed from the base of the PR and between 1c577f0 and 8a6eb69.

⛔ Files ignored due to path filters (6)
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • tests/conformance/ch06_assert_assume.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (10)
  • CHANGELOG.md
  • HISTORY.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/conformance/manifest.json
  • tests/test_soundness_392.py
  • vera/__init__.py
  • vera/verifier.py

Comment thread README.md Outdated
Comment thread tests/test_soundness_392.py
Comment thread vera/verifier.py
aallan and others added 2 commits June 26, 2026 12:24
…able discharge

The pr-review-toolkit (pr-test-analyzer, criticality 7) flagged that `match`
arms reach the #804 logic through a route distinct from if/else (the nat-binding
walk shares the parent assumptions to each arm; the primitive-op walk scopes via
the per-block del), and that div_zero discharge by a prior assert was untested.

Adds 4 tests to TestAssertAssumeFacts804:
- within-match-arm forward discharge ([tier3, verified])
- cross-match-arm non-leak soundness guard ([tier3, tier3])
- div_zero discharged by a prior assert(@Int.0 != 0) (removes a false E526 on
  the guard-then-divide idiom)
- an untranslatable assert between two translatable ones does not break forward
  discharge (the _assumed_block_fact None-return path)

The 3 behavior tests were confirmed RED on the pre-fix verifier (revert
vera/verifier.py to 1c577f0); the cross-arm guard is green both ways. Test-only;
no behaviour change. Part of the v0.0.179 release (#804).

Co-Authored-By: Claude <noreply@anthropic.invalid>
…ollow-ups

Addresses the pr-review-toolkit + CodeRabbit review of #805:

- Call preconditions (CR Major): a call's precondition is checked during body
  translation (#730), one phase before the obligation walks, so the #804 fact
  pushes didn't reach it — a guarded call (assert P; then a call requiring P)
  recorded a false E501. SmtContext._translate_block now threads the assert/
  assume fact forward: pushed onto _path_conditions after the statement, dropped
  at block exit by a finally that also covers the early return-None paths (so an
  untranslatable statement after an assert can't leak the fact into the
  post-translation walks). Confirmed RED on the pre-fix verifier, with a
  soundness guard that a weaker prior assert still records E501.
- assume coverage (CR Minor): added top-level assume() postcondition and
  refined-return discharge tests (mirroring the assert cases).
- doc sync (CR Minor): HISTORY "By the numbers" total 174 -> 179 tagged releases
  (matches the README status line + git tag count); README test count -> 4,938.

test_soundness_392.py -> 35 tests; full suite 4906 passed. Part of v0.0.179.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

pr-review-toolkit — round 1

Ran four review agents (code-reviewer, pr-test-analyzer, silent-failure-hunter, comment-analyzer) over the diff. Each validated empirically — mutation tests plus vera verify / vera run differentials — not just by reading the rationale.

Verdict: no bugs, no soundness defects, comments accurate

  • code-reviewer — recommends merge. Mutation-validated both soundness axes: pushing the fact before the per-statement walk reddens the forward-only guard; removing the per-block del reddens the cross-branch guard. The no-try/finally choice on the fact pushes is sound — verified the "every function starts from a reset _path_conditions" invariant on both the warm (smt.reset()) and cold (fresh SmtContext) paths.
  • silent-failure-hunter — no silent-failure or soundness defect; the change degrades conservatively. Verified: an untranslatable predicate drops its fact (incompleteness, never a false Tier-1 proof); del [pc_depth:] removes only this block's facts (recursive if/match pushes are try/finally-balanced); a leaked fact on exception can't survive (a poison-False fact on the warm path still left the next function starting at _path_conditions length 0). Master differential: every Tier-1 discharge traps at the guarding assert on a violating input under vera run.
  • comment-analyzer — every new comment accurate; the cited spec sections (§6.4.1, §11.14.1) resolve with verbatim-correct formulas, and the test docstrings' pre-fix predictions (exact error codes + counterexamples) reproduce exactly.
  • pr-test-analyzer — merge-ready; surfaced coverage hardening (below).

What I changed in response (commit f522200)

No defects to fix, but the test-analyzer surfaced two genuine coverage gaps worth closing, so I added 4 tests (test_soundness_392.py → 31):

  1. match-arm scoping (criticality 7) — match arms reach the verifier: body asserts should be assumed as facts for subsequent obligations (spec §2.8 assume-part of #800) #804 logic through structurally different code than if/else, and a cross-arm leak would be an unsound failure mode the if/else tests don't cover. Added a within-arm forward-discharge test and a cross-arm non-leak soundness guard.
  2. div_zero discharge (criticality 4) — the most user-facing forward discharge: assert(@Int.0 != 0); 100 / @Int.0 now discharges the division's obligation at Tier 1 (removes a false E526 on guard-then-divide). Added a test.
  3. Untranslatable-assert sandwich (criticality 4) — exercises the _assumed_block_fact None-return path: an untranslatable assert between two translatable ones adds no fact but does not corrupt the later discharge. Added a test.

The 3 behavior tests were confirmed RED on the pre-fix verifier (reverting vera/verifier.py to 1c577f0); the cross-arm guard is green both ways.

Considered and left as-is (sub-threshold, verified non-issues)

  • comment-analyzer's one advisory — the 5.8–7b range label on the cleanup comment (a range that already includes the in-between steps).
  • code-reviewer's two low-confidence notes — a defensive Bool-sort guard on a path the checker already rejects with E172 before verification, and the collector's deliberately-conservative env-threading (a completeness, never soundness, difference already called out in its docstring).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_soundness_392.py`:
- Around line 489-507: The test for
`test_prior_assert_discharges_call_precondition` is too weak because it only
checks that `needs_positive` produces no error and no `call_pre` obligation,
which can still pass if the call never reaches Tier-1 translation. Strengthen
the test by adding an observable callee-to-caller assertion tied to
`needs_positive` (for example, a postcondition or refined return that `f` must
verify) so the test proves the call was translated and discharged. Apply the
same pattern to the corresponding `assume(...)` case if present, using the
existing `_verify`, `result.obligations`, and `result.diagnostics` assertions as
the entry points.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57dca4f8-9697-4e7b-96a9-695d9b3b1270

📥 Commits

Reviewing files that changed from the base of the PR and between 8a6eb69 and 4875661.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • HISTORY.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_soundness_392.py
  • vera/smt.py

Comment thread tests/test_soundness_392.py
CodeRabbit round-2 (Major): the call-pre discharge test asserted only "no E501"
+ "no call_pre obligation", which would still pass if the call silently stopped
translating (successful call-pre checks are unrecorded). Tie the result to the
callee's postcondition — the caller's ensures(result > 0) only verifies if the
call is actually translated and discharged — so the test fails (ensures ->
tier3) on a translation regression, not merely on a missing error. Mirror the
shape for the assume half (a new assume call-pre test).

Both confirmed RED on the pre-call-pre-fix smt.py. test_soundness_392.py -> 36.

Co-Authored-By: Claude <noreply@anthropic.invalid>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@TESTING.md`:
- Line 63: Update the TESTING.md entry so the assume-half behavior is attributed
to the correct issue number. The current note in the release/test matrix points
to `#800`, but the surrounding release notes and PR context identify the
assume-half rule as `#804`; change that reference in the TESTING.md row for
test_soundness_392.py so readers are directed to the right regression/change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cefe9932-b0a5-40c3-bc8f-6b2ddc3edf8e

📥 Commits

Reviewing files that changed from the base of the PR and between 4875661 and 5d5000b.

📒 Files selected for processing (4)
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_soundness_392.py

Comment thread TESTING.md Outdated
CodeRabbit round-3: the test_soundness_392.py row said "the #800 assume-half",
which misdirects readers to #800 (the prove-half); the assume-half is #804. Lead
the clause with #804 and note it is the assume-half of #800's assert rule. Also
reflect the call-precondition discharge (E501) added earlier this round.

Validated --no-verify (prose-only): doc-counts + site-assets green; no vera/spec
change so suite/mypy/ruff/changelog gates are N/A.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan aallan merged commit ad5967d into main Jun 26, 2026
26 checks passed
@aallan aallan deleted the fix/804-assert-assume-facts branch June 26, 2026 14:58
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.

verifier: body asserts should be assumed as facts for subsequent obligations (spec §2.8 assume-part of #800)

1 participant