Skip to content

feat(680): auto-synthesise division/modulo + array-index safety obligations (close last Tier-0 silent failure)#778

Merged
aallan merged 19 commits into
mainfrom
feat/680-primitive-op-obligations
Jun 22, 2026
Merged

feat(680): auto-synthesise division/modulo + array-index safety obligations (close last Tier-0 silent failure)#778
aallan merged 19 commits into
mainfrom
feat/680-primitive-op-obligations

Conversation

@aallan

@aallan aallan commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Closes the last Tier-0 silent failure (ROADMAP): the verifier now auto-synthesises a Tier-1 proof obligation at every trapping primitive operation whose well-definedness depends on operand values, so vera verify clean ⟹ holds at runtime for these ops too.

What changed

Operation Obligation Outcome
a / b, a % b (Int/Nat) b != 0 Tier-1-decidable → loud E526 when the divisor isn't proven non-zero (float division exempt — f64.div by zero yields inf/NaN, not a trap)
arr[i] (Array<T>) 0 <= i < array_length(arr) Tier-1 where a literal / refinement / precondition / path-condition pins the length; loud E527 where provably out of bounds ([1, 2, 3][5]); else honest Tier-3 (runtime-guarded)

These join the existing @Nat subtraction (E502) and narrowing (E503) obligations. The strictness is read off the Tier-1/Tier-3 boundary: division's divisor is a concrete Z3 Int (decidable), but array_length is an uninterpreted function (beyond Tier 1, per spec §6.4.4 and #427), so the index obligation degrades gracefully rather than firing false positives on safe-but-dynamic indices.

Implementation

  • The @Nat subtraction walker generalises to one _walk_for_primitive_op_obligations pass (vera/verifier.py) with two new leaf checks: _check_div_zero_obligation (mirrors the Nat subtraction silently underflows to negative i64 — refinement-type soundness hole #520 verified / violated / timeout template) and _check_index_bounds_obligation (a two-check: prove-in-bounds → Tier 1; prove-out-of-bounds → loud E527; else Tier 3).
  • Codegen is unchanged — the divide_by_zero / out_of_bounds runtime traps already existed; this adds only the static layer.
  • New error codes E526 / E527; new ObligationKinds div_zero / index_bounds.

Tests & docs

  • 19 new verifier tests (TestPrimitiveDivisionObligation680, TestPrimitiveIndexObligation680) covering the loud / Tier-1 / Tier-3 / exemption cases — including a differential that pins an opaque index as a recorded Tier-3 obligation (not silently dropped), and one pinning that closure-body indexes stay unobligated (the Implement Tier 2 verification (Z3-guided with hints from assert/lemma) #427 boundary).
  • tests/conformance/ch04_primitive_obligations.vera (verifies clean, 18 Tier-1 obligations).
  • Zero corpus regressions: all 35 examples + 92 conformance programs pass; the example tier ledger gains +3 Tier-1 (guarded divisions) and +5 Tier-3 (opaque indexes), no new errors.
  • spec §6.4.3 rewritten (obligations now auto-synthesised), README and FAQ updated, ROADMAP Tier 0 — Close the silent failures marked done. Release v0.0.177.

Dynamic and closure-captured array bounds remain runtime-guarded Tier 3; lifting them to a Tier-1 proof is the Tier 2 work in #427.

Closes #680

Summary by CodeRabbit

Release Notes – Version 0.0.177

  • New Features

    • The verifier now auto-synthesises primitive safety obligations for integer division/modulo by zero (E526) and array index bounds (E527). Provably unsafe cases now fail at compile time; otherwise behaviour is tiered with runtime-guarded traps. Float divide-by-zero remains exempt.
  • Documentation

    • Updated the primitive-operation safety spec and guidance (FAQ/README/ROADMAP/TESTING/SKILL), plus new/clarified error-code mappings and tiering notes.
    • Conformance suite documentation aligned to 92 programs.
  • Tests

    • Added regression coverage for E526/E527 and refreshed conformance/test expectations and counts.
  • Chores

    • Version bumped to 0.0.177.

Close the last Tier-0 silent failure: the verifier now emits a Tier-1
proof obligation at every integer division/modulo (b != 0, E526) and
array index (0 <= i < array_length, E527), discharged from preconditions
and path conditions like the existing @nat subtraction (E502) and
narrowing (E503) obligations. vera verify no longer reports a program
clean that then traps at runtime.

Division/modulo are Tier-1-decidable, so an unguarded divisor the solver
can't prove non-zero is a compile error (float division exempt -- no
trap). array_length is uninterpreted, so the index obligation tiers
honestly via a two-check: Tier-1 where a literal/refinement/precondition/
path-condition pins the length, loud E527 where provably out of bounds
(e.g. [1,2,3][5]), else runtime-guarded Tier-3 (#427 lifts the dynamic
and closure-captured cases). Codegen is unchanged -- the divide_by_zero
and out_of_bounds runtime traps already existed.

The @nat subtraction walker generalises to one
_walk_for_primitive_op_obligations pass over all operand-trapping sites.
Adds 19 verifier tests + a ch04 conformance program; rewrites spec
6.4.3, README, FAQ; ROADMAP Tier-0 section marked done. Release v0.0.177.

Co-Authored-By: Claude <[email protected]>
@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.44262% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.77%. Comparing base (cf1ec81) to head (9db0226).

Files with missing lines Patch % Lines
vera/verifier.py 93.29% 12 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main     #778    +/-   ##
========================================
  Coverage   91.77%   91.77%            
========================================
  Files          71       71            
  Lines       26239    26398   +159     
  Branches      321      321            
========================================
+ Hits        24080    24228   +148     
- Misses       2151     2162    +11     
  Partials        8        8            
Flag Coverage Δ
javascript 65.33% <ø> (ø)
python 94.86% <93.44%> (-0.02%) ⬇️

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 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements auto-synthesised proof obligations for integer division/modulo-by-zero (E526) and array index bounds (E527), replacing the subtraction-only verifier walker with a unified primitive-op walker that tracks path conditions across branches. Adds two new ObligationKind literals and error codes, discharge helpers with Tier-1/Tier-3 fallback, diagnostic emitters, 27 new verifier tests, a new conformance manifest entry, spec §6.4.3 rewrite, and a version bump to 0.0.177 with comprehensive documentation updates reflecting the 92-programme conformance suite.

Changes

Primitive Operation Obligation Synthesis

Layer / File(s) Summary
ObligationKind vocabulary and error code registry
vera/obligations/core.py, vera/errors.py
ObligationKind gains div_zero and index_bounds Literal members with Tier-1/Tier-3 tiering annotations; ERROR_CODES gains E526 ("Division or modulo by zero") and E527 ("Array index out of bounds").
Verifier Phase 5.5 wiring, unified primitive-op walker, and error formatting
vera/verifier.py, vera/ast.py
_verify_fn Phase 5.5 is rewired to call _walk_for_primitive_op_obligations instead of the subtraction-only walker; the new walker retains @Nat - @Nat`` underflow handling (E502), adds div/mod-by-zero and index-bounds obligation emission, and manages smt._path_conditions push/pop across `IfExpr` and `MatchExpr` arm bodies to ensure obligations discharge under correct branch guards. Expression traversal routes into array indexing (`IndexExpr`), array literals, assert/assume conditions, and interpolated strings whilst deliberately excluding closure, quantifier, and handler-clause bodies. Opaque shadow tracking prevents stale-slot facts from producing false static discharges. ArrayLit error message formatting is extended to support diagnostic output.
Obligation discharge helpers and diagnostic emitters
vera/verifier.py
_is_opaque_shadow and _contains_opaque_shadow recognise fresh Z3 constants that track untranslatable rebinds. _check_subtraction_obligation downgrades to Tier-3 when operands are opaque shadows. _check_div_zero_obligation translates divisor to Z3, discharges divisor != 0 as Tier-1 goal for decidable integer divisors, falls back to Tier-3 for floats/untranslatable operands. _check_index_bounds_obligation performs Tier-1 bounds reasoning 0 ≤ i < array_length(arr) only for recognised array sorts, records Tier-3 obligations for dynamic/opaque/untranslatable indices, emits E527 for provably out-of-bounds. Both discharge paths delegate to _report_div_by_zero (E526) and _report_index_oob (E527) diagnostic emitters that format counterexample-aware rationale and fix text. _fresh_pattern_env gains track flag to register pattern vars as opaque shadows when scrutinee is untranslatable.
Spec §6.4.3 rewrite and example allowlist adjustment
spec/06-contracts.md, scripts/check_spec_examples.py
Section 6.4.3 "Primitive Operation Safety" is rewritten with an obligations table mapping primitives to operand constraints and error codes (E502/E503/E526/E527), explicit Tier-1 rules for decidable div/mod non-zero divisors and statically pinned array lengths, Tier-3 runtime-guarded fallback for opaque/dynamic array lengths and compile-time E527 emission for provably out-of-bounds, and refreshed @Nat underflow and narrowing coverage specifying which binding sites are obligated versus not obligated. SafeDiv FRAGMENT allowlist entry moves from line 340 to 344 to align with rewritten spec prose.
Conformance manifest and verifier test suite
tests/conformance/manifest.json, tests/test_verifier.py
New ch04_primitive_obligations manifest entry at verify level references §6.4.3 with division, modulo, array-indexing, obligation, precondition, and discharge-related feature tags. TestPrimitiveDivisionObligation680 (20 test methods) covers unguarded @Int/@Nat division/modulo failures with E526, discharge via requires(divisor != 0), if guards, and @PosInt refinement types, literal/float exemptions, non-discharge when guarding numerator, obligation recording assertions, and nested contexts (array literals, assert conditions, interpolated strings). TestPrimitiveIndexObligation680 (15 test methods) covers literal in-bounds discharge, provably out-of-bounds failures (strictness at equality, provably-negative indices), discharge via requires/if guards, Tier-3 obligations for refinement/opaque/dynamic unguarded indices, explicit non-obligation inside array_map closure bodies, and index sub-expression obligation traversal. Tier-count aggregate assertions updated.
Package version and release metadata
pyproject.toml, vera/__init__.py, CHANGELOG.md, HISTORY.md
Package version incremented 0.0.1760.0.177. CHANGELOG adds section ## [0.0.177] - 2026-06-21 documenting auto-synthesised Tier-1 obligations (E526, E527) and spec §6.4.3 update. HISTORY adds v0.0.177 entry to Stage 14. Comparison-reference links updated ([Unreleased]v0.0.177...HEAD, new [0.0.177]v0.0.176...v0.0.177).
User-facing documentation: README, ROADMAP, FAQ
README.md, ROADMAP.md, FAQ.md
README "Contracts the compiler proves" section states compiler synthesises primitive-op obligations, replacing prior claim that missing guards only trap at runtime. ROADMAP Tier 0 section expanded into narrative on closing silent weaker-than-promised behaviours, explicitly covering Tier 1 (decidable division/modulo), compile-time rejection (E526/E527), and Tier 3 runtime guarding for dynamic array indices; Tier 1 issues table adds #387 (mutation testing) and removes it from "Ongoing threads" CI list. FAQ "Does the compiler prove division-by-zero…?" section rewritten to describe auto-synthesised obligations with explicit codes and discharge mechanisms (preconditions, guards, refinement types); conformance suite count updated 91 → 92 in two places.
Reference documentation and conformance suite inventory
AGENTS.md, CLAUDE.md, SKILL.md, TESTING.md, KNOWN_ISSUES.md
Conformance suite counts updated 91 → 92 across all reference files. TESTING.md metrics, validation scripts, test file rows, conformance suite/levels sections, directory structure, and pre-commit hooks all reflect updated inventory (test_verifier coverage expanded, test_conformance parametrisation 455 → 460, verify-level programme count 7 → 8). KNOWN_ISSUES.md expands existing E501 limitations (untranslatable let/LetDestruct and effect-op argument cases) and adds new entry documenting non-emission of primitive-op obligations inside closure/quantifier/handler-clause bodies as reporting-completeness gap (issue #779).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #680 — This PR directly implements auto-injection of proof obligations for division, modulo, and array indexing described in issue #680, bringing verifier behaviour into alignment with the README's static-safety claim that "division by zero is not a runtime error — it is a type error."
  • #779 — Tracks the next phase to extend primitive-op obligation walkers into closure/quantifier/handler-clause bodies currently excluded by this PR.

Possibly related PRs

  • aallan/vera#554: The unified _walk_for_primitive_op_obligations directly builds upon and generalises the subtraction-only walker introduced in PR #554, retaining its @Nat - @Nat`` underflow logic (E502) whilst extending coverage to division/modulo and array indexing.

Suggested labels

compiler, tests, spec, docs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the primary change: auto-synthesising division/modulo and array-index safety obligations to close Tier-0 silent failures, matching the issue #680 objectives.
Linked Issues check ✅ Passed All primary objectives from #680 are met: division (E526) and modulo obligations auto-inject, array indexing (E527) obligations auto-inject, float division is exempted, Tier-1 discharge occurs via preconditions/path conditions, Tier-3 fallback for opaque operands, and spec section 6.4.3 is rewritten. The conformance suite gains one new program. Testing shows 19 new verifier tests with comprehensive coverage of loud errors, discharge paths, and Tier-3 cases.
Out of Scope Changes check ✅ Passed All changes directly support #680 obligations work: verifier walker extension, obligation kind registration, error code definitions, test suite expansion, documentation updates for 92-program conformance, and version bump. No unrelated features introduced.
Docstring Coverage ✅ Passed Docstring coverage is 98.18% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/680-primitive-op-obligations

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
vera/verifier.py (2)

2033-2057: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Use fresh pattern bindings when the match scrutinee is opaque.

When scrutinee_z3 is None, arm_env remains slot_env. A branch like Some(@int) -> 10 / @Int.0`` can then prove the divisor obligation against an outer @Int.0, not the pattern payload, so an effect/call-returned `Some(0)` can still trap after a clean verify. Use `_fresh_pattern_env(...)` here, as the nat-binding walker already does for the same shadowing case.

Suggested fix
                 if scrutinee_z3 is not None:
                     bound = smt._bind_pattern(
                         scrutinee_z3, arm.pattern, slot_env,
                     )
                     if bound is not None:
                         arm_env = bound
                     pat_cond = smt._pattern_condition(
                         scrutinee_z3, arm.pattern,
                     )
+                else:
+                    arm_env = self._fresh_pattern_env(
+                        arm.pattern, slot_env, smt,
+                    )
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 2033 - 2057, When the match scrutinee is
opaque (scrutinee_z3 is None), the code currently leaves arm_env as slot_env,
which causes pattern variables to shadow outer scope variables and leads to
incorrect verification. Modify the pattern handling logic in this match
expression block so that when scrutinee_z3 is None, call _fresh_pattern_env()
with the pattern and slot_env to create fresh bindings for the pattern
variables, similar to how the nat-binding walker handles the same shadowing
case. This ensures that proof obligations are verified against the correct
pattern payloads, not outer variables.

1960-1980: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Invalidate shadowing block bindings before checking later primitive ops.

Line 1967 only pushes a let binding when its RHS translates. If the RHS is untranslatable, later @Int.0/@Array<T>.0 references in the block resolve to an outer binding, so an outer requires(@Int.0 != 0) can incorrectly discharge 10 / @Int.0`` after let @Int` = E.next(())`. `LetDestruct` is also skipped entirely, which misses RHS primitive ops and leaves destructured slots stale. Mirror the fresh-rebind approach from the nat-binding walker, including fresh array variables for index checks.

One possible direction
                 if isinstance(stmt, ast.LetStmt):
                     self._walk_for_primitive_op_obligations(
                         decl, stmt.value, smt, cur_env, assumptions,
                     )
                     val = smt.translate_expr(stmt.value, cur_env)
+                    if val is None:
+                        let_ty = self._resolve_type(stmt.type_expr)
+                        if self._is_array_type(let_ty):
+                            val = self._declare_array_var(
+                                smt, smt._fresh_name("primitive_let"), let_ty,
+                            )
+                        else:
+                            val = self._fresh_slot_var(smt, stmt.type_expr)
                     if val is not None:
                         type_name = smt._type_expr_to_slot_name(stmt.type_expr)
                         if type_name is not None:
                             cur_env = cur_env.push(type_name, val)
+                elif isinstance(stmt, ast.LetDestruct):
+                    self._walk_for_primitive_op_obligations(
+                        decl, stmt.value, smt, cur_env, assumptions,
+                    )
+                    # Rebind destructured slots to exact literal components
+                    # when available, otherwise fresh placeholders, before
+                    # walking subsequent statements.
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 1960 - 1980, The block walker currently only
pushes a let binding when the RHS translates successfully, which allows later
primitive op checks to incorrectly resolve to outer bindings. Additionally,
LetDestruct statements are not handled at all. To fix this: (1) Ensure that for
each LetStmt, a fresh binding is pushed into cur_env regardless of whether the
RHS translates (similar to the nat-binding walker approach), (2) Add handling
for ast.LetDestruct statements to walk their RHS for primitive ops and push
fresh bindings for the destructured slots, (3) Include fresh array variables for
index checks when pushing bindings, and (4) Use this updated cur_env when
walking the final expr.expr to ensure shadowed bindings are properly
invalidated.
🤖 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 `@FAQ.md`:
- Around line 53-59: The FAQ section currently states that division and modulo
by zero carry an E526 obligation without clarifying that this applies only to
integer and natural division. Revise the text in the first paragraph where it
describes division and modulo obligations to explicitly scope the E526
obligation to integer and natural division only, and add a clarification that
floating-point division is exempt from this requirement because it returns inf
or NaN instead of trapping. This distinction should appear early in the
explanation where division is first mentioned.

In `@ROADMAP.md`:
- Line 19: The statement "Every trapping primitive operation" in the ROADMAP.md
entry is too broad and overstates the scope of this PR. Narrow the guarantee to
specifically reference only the primitive operations that this PR actually
covers: `@Nat` underflow and narrowing, integer division and modulo by zero, and
array index bounds. Either explicitly name the remaining runtime-guarded
primitives that are not covered (such as float-to-int conversion) or clarify
that the statement applies only to the auto-synthesised obligations added in
this specific PR, making the scope more accurate and honest about what has been
implemented versus what still remains as Tier 3.

In `@TESTING.md`:
- Around line 61-62: The `test_verifier.py` row in the TESTING.md documentation
table does not include information about the new primitive-obligation coverage
(div/mod-by-zero and array-bounds obligation tests) added by this PR. Update the
`test_verifier.py` entry in the table to add mention of these new 19 verifier
tests covering primitive-operation obligations so the documentation accurately
reflects the current test suite coverage.

In `@tests/test_verifier.py`:
- Around line 1028-1040: The test method test_index_inside_closure_not_obligated
pins the deliberate non-recursion into closure bodies, but the specification has
been updated to require that index accesses inside closures and quantifier
bodies should now emit index_bounds obligations with tier3 status rather than
being skipped. Update this test to verify that the index access at
`@Array`<Int>.0[0] inside the array_map closure now generates a tier3 obligation
instead of being obligated, aligning with the spec change in section 6.4.3.
Consider renaming the test method to reflect the new expected behavior and
adjust the _verify_ok call to validate the presence of the tier3 index_bounds
obligation or change the verification approach accordingly.

In `@vera/errors.py`:
- Line 512: The E527 error message definition in the errors dictionary currently
includes both "Array or string index out of bounds", but string indexing is
handled separately as a type error (E161). Update the E527 error message
definition to specify only array indexing behavior, removing the reference to
string indexing to maintain semantic accuracy of the diagnostics.

In `@vera/verifier.py`:
- Around line 1393-1395: The _walk_for_primitive_op_obligations function
currently only scans decl.body for primitive operations, but it needs to also
scan primitive operations within contract expressions (requires, ensures,
decreases clauses). In addition to the existing call that walks decl.body, you
need to separately walk each contract expression (access the requires, ensures,
and decreases attributes from the decl object) at the point they are discharged,
using only the assumptions that are valid before each contract expression is
assumed. This ensures that division-by-zero and other primitive operation issues
within contracts are properly caught before they are used as assumptions in
verification.
- Around line 2079-2081: The fallback comment and return statement around line
2079-2081 incorrectly assumes expression containers like ArrayLit,
InterpolatedString, and closure/quantifier bodies contain no nested primitive
operations. This is false—these containers can hold division or index operations
that need codegen evaluation. Remove or modify the fallback termination and add
explicit branches to handle ArrayLit (processing its elements),
InterpolatedString (processing its interpolation parts), and closure/quantifier
bodies. For closures and quantifiers, either add the necessary handling or add a
clear comment explicitly stating if they are intentionally Tier 3 and why they
don't need this processing, rather than silently treating them as having no
nested operations.

---

Outside diff comments:
In `@vera/verifier.py`:
- Around line 2033-2057: When the match scrutinee is opaque (scrutinee_z3 is
None), the code currently leaves arm_env as slot_env, which causes pattern
variables to shadow outer scope variables and leads to incorrect verification.
Modify the pattern handling logic in this match expression block so that when
scrutinee_z3 is None, call _fresh_pattern_env() with the pattern and slot_env to
create fresh bindings for the pattern variables, similar to how the nat-binding
walker handles the same shadowing case. This ensures that proof obligations are
verified against the correct pattern payloads, not outer variables.
- Around line 1960-1980: The block walker currently only pushes a let binding
when the RHS translates successfully, which allows later primitive op checks to
incorrectly resolve to outer bindings. Additionally, LetDestruct statements are
not handled at all. To fix this: (1) Ensure that for each LetStmt, a fresh
binding is pushed into cur_env regardless of whether the RHS translates (similar
to the nat-binding walker approach), (2) Add handling for ast.LetDestruct
statements to walk their RHS for primitive ops and push fresh bindings for the
destructured slots, (3) Include fresh array variables for index checks when
pushing bindings, and (4) Use this updated cur_env when walking the final
expr.expr to ensure shadowed bindings are properly invalidated.
🪄 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: 9430f33f-629d-4a0e-9b51-8529bf5cb5f4

📥 Commits

Reviewing files that changed from the base of the PR and between cf1ec81 and 3848f0d.

⛔ Files ignored due to path filters (7)
  • docs/SKILL.md is excluded by !docs/**
  • 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/ch04_primitive_obligations.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (18)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • HISTORY.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_spec_examples.py
  • spec/06-contracts.md
  • tests/conformance/manifest.json
  • tests/test_verifier.py
  • vera/__init__.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

Comment thread FAQ.md Outdated
Comment thread ROADMAP.md Outdated
Comment thread TESTING.md Outdated
Comment thread tests/test_verifier.py
Comment thread vera/errors.py Outdated
Comment thread vera/verifier.py
Comment thread vera/verifier.py Outdated
…assert, 6 tests, doc accuracy

Address the PR #778 review (no soundness hole found; all items are
completeness / test / accuracy):

- Walker: recurse into ArrayLit elements + Assert/Assume conditions, so an
  unguarded `[a/b, ...]` or `assert(a/b > 0)` is now E526 (was clean + a
  runtime trap). Zero corpus regression; the example tier ledger is
  unchanged. Closure / quantifier / handler-clause bodies remain
  runtime-guarded -- filed #779.
- Tests (+6): index == length (off-by-one), provably-negative index, an
  @int upper-bound-only guard stays Tier-3 (pins the i >= 0 conjunct),
  op-inside-index walked, division in array-literal + assert. Strengthen
  the closure test with an index_bounds == 0 differential (it passed for
  the wrong reason -- a walker breach degrades to Tier-3, still no error).
- Accuracy: E527 title + index_bounds kind drop "string" (String indexing
  is E161); fix the conformance file's reversed divisor/dividend De Bruijn
  label; broaden the dispatch comment, section banner, and walker docstring
  to cover div/mod/index; correct the _report_index_oob rationale (also
  fires on a provably-negative index) and the array_length >= 0 docstring
  overclaim; TESTING.md 506/606 conformance count 91 -> 92.

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 21, 2026

Copy link
Copy Markdown
Owner Author

pr-review-toolkit review (4 agents) — addressed in 4d9a7e4

Ran silent-failure-hunter, code-reviewer, pr-test-analyzer, and comment-analyzer against the diff.

Soundness: clean. The silent-failure-hunter ran an adversarial audit and could not construct a program that verifies clean and then silently misbehaves — the two-check, the float-gating, the bookkeeping, and the provably-negative / off-by-one index paths are all sound. Findings were completeness, test-strength, and doc-accuracy.

Walker completeness (silent-failure-hunter, HIGH)

The walker skipped ArrayLit elements and Assert / Assume conditions, so an unguarded [@Int.0 / @Int.1, 99] or assert(@Int.0 / @Int.1 > 0) verified clean (then trapped at runtime) — inconsistent with the "division → compile error" promise. Fixed: the walker now recurses into those positions (both fire E526). Zero corpus regression, tier ledger unchanged. The remaining fresh-scope bodies (closure / quantifier / handler clause) stay runtime-guarded and are tracked in #779 (filed) + KNOWN_ISSUES.md.

Test strength (pr-test-analyzer, two criticality-8 gaps)

  • Off-by-one: nothing pinned the strict < in i < length — added [1, 2, 3][3] (index == length) → E527.
  • Lower bound: nothing pinned the i >= 0 conjunct (every fixture used @Nat) — added a provably-negative index → E527, and an @Int upper-bound-only guard → stays Tier-3 (not a false Tier-1).
  • Added op-inside-index (arr[@Int.0 / @Int.1] → E526) and the array-literal / assert cases.
  • The closure non-obligation test passed for the wrong reason (a walker breach degrades to Tier-3, still no error) — strengthened with an index_bounds == 0 differential.

Accuracy (comment-analyzer + code-reviewer)

  • E527 title and the index_bounds kind comment dropped "string" (String indexing is a type error, E161 — never reaches E527).
  • Fixed the conformance file's reversed divisor / dividend De Bruijn label.
  • Broadened the dispatch comment, section banner, and walker docstring (still described subtraction-only); corrected the _report_index_oob rationale (also fires on a provably-negative index) and an array_length >= 0 docstring overclaim.
  • TESTING.md had two stale 9192 conformance counts the count-gate doesn't cover.

One nit was skipped: a localised opaque-divisor Tier-3 unit test (criticality 6). A genuinely-untranslatable integer divisor is hard to construct (most expressions translate), and the path is already exercised by the full-corpus differential in test_obligations.py (json.vera's opaque divisor).

CI green; all gates pass.

@aallan

aallan commented Jun 21, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
vera/verifier.py (3)

3943-3949: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Render the actual divisor in the E526 fix hint.

The current hint hard-codes @Int.1, but the divisor may be @Int.0, a @Nat, or a compound expression. That can send users to guard the wrong De Bruijn slot.

Suggested fix
+        divisor_src = ast.format_expr(expr.right)
+        operation_src = ast.format_expr(expr)
         self._error(
             expr,
             description,
@@
             fix=(
-                "Add a precondition ruling out a zero divisor, e.g. "
-                "`requires(`@Int.1` != 0)`.  Alternatively, guard the "
-                "operation (`if `@Int.1` == 0 then 0 else `@Int.0` / `@Int.1`` — "
+                "Add a precondition or guard that proves the divisor is "
+                f"non-zero, e.g. `{divisor_src} != 0`.  Alternatively, guard "
+                f"the operation (`if {divisor_src} == 0 then 0 else "
+                f"{operation_src}` — "
                 "the path condition discharges the obligation in the "
                 "else-branch) or give the divisor a refinement type such "
                 "as `{ `@Int` | `@Int.0` != 0 }`."
             ),
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 3943 - 3949, The E526 division by zero error
hint in the fix string hard-codes `@Int.1` as the divisor, but the actual
divisor could be `@Int.0`, `@Nat`, or a compound expression, which misleads
users about which De Bruijn slot to guard. Locate where the fix hint is
constructed for this error, capture the actual divisor expression being used in
the division operation, and dynamically insert the rendered divisor into the
hint message instead of hard-coding `@Int.1`, ensuring the message references
the correct divisor value.

2040-2064: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fresh-shadow match bindings when the scrutinee is opaque.

If smt.translate_expr(expr.scrutinee, slot_env) returns None, arm_env remains the outer environment. A pattern binding such as Some(@int) can then leave @Int.0 in the arm body pointing at an outer parameter, so 1 / @Int.0`` may verify against the caller’s precondition instead of the matched payload. Mirror the narrowing walker’s _fresh_pattern_env(...) fallback here.

Suggested fix
                 if scrutinee_z3 is not None:
                     bound = smt._bind_pattern(
                         scrutinee_z3, arm.pattern, slot_env,
                     )
                     if bound is not None:
                         arm_env = bound
                     pat_cond = smt._pattern_condition(
                         scrutinee_z3, arm.pattern,
                     )
+                else:
+                    arm_env = self._fresh_pattern_env(
+                        arm.pattern, slot_env, smt,
+                    )
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 2040 - 2064, When scrutinee_z3 is None
(meaning the scrutinee is opaque and cannot be translated), the current code
leaves arm_env as the outer slot_env, which allows pattern bindings like
Some(`@Int`) to incorrectly reference outer parameters instead of the matched
payload. To fix this, when scrutinee_z3 is None, create a fresh pattern
environment for arm_env similar to how the narrowing walker handles this case
with _fresh_pattern_env(...). This ensures pattern variables are properly
isolated from the outer scope and verified against the matched payload rather
than the caller's precondition.

1967-1984: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Mirror block binding semantics before checking later primitive ops.

When a let RHS is untranslatable, this walker does not push a fresh shadow slot, so a later 1 / @Int.0`` can resolve to an outer parameter and discharge from the wrong requires. The same block also skips `LetDestruct` entirely, missing trapping ops in the destructure RHS and any slots it introduces. That can turn an effect-produced zero divisor or out-of-bounds array into a false Tier-1 verification.

Suggested direction
                 if isinstance(stmt, ast.LetStmt):
                     self._walk_for_primitive_op_obligations(
                         decl, stmt.value, smt, cur_env, assumptions,
                     )
                     val = smt.translate_expr(stmt.value, cur_env)
+                    if val is None:
+                        val = self._fresh_slot_var(smt, stmt.type_expr)
+                    if val is None:
+                        type_name = smt._type_expr_to_slot_name(stmt.type_expr)
+                        stale = (
+                            cur_env.resolve(type_name, 0)
+                            if type_name is not None else None
+                        )
+                        if stale is not None:
+                            val = z3.FreshConst(stale.sort(), prefix="letbind")
                     if val is not None:
                         type_name = smt._type_expr_to_slot_name(stmt.type_expr)
                         if type_name is not None:
                             cur_env = cur_env.push(type_name, val)
+                elif isinstance(stmt, ast.LetDestruct):
+                    self._walk_for_primitive_op_obligations(
+                        decl, stmt.value, smt, cur_env, assumptions,
+                    )
+                    # Rebind each destructured slot to either its translated
+                    # literal component or a fresh shadow value, matching the
+                    # stale-slot invalidation used by the narrowing walker.
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 1967 - 1984, The block walker does not
maintain correct variable scoping when translating let statement RHS values.
When val is None (untranslatable RHS), the code skips pushing a fresh shadow
slot, causing later expressions to incorrectly resolve to outer parameters
instead of the current scope. Additionally, the code completely ignores
LetDestruct statements, missing trapping operations in their RHS and any slots
they introduce. Fix this by ensuring that even when val is None, a shadow slot
is still pushed for the let binding in the cur_env to maintain correct scoping
semantics. Also add handling for ast.LetDestruct statements by walking their RHS
for primitive op obligations and pushing appropriate bindings to cur_env,
similar to the LetStmt handling.
♻️ Duplicate comments (1)
TESTING.md (1)

61-62: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the primitive-obligation coverage to test_verifier.py.

The row still omits the new div/mod-by-zero and array-bounds cases added by this PR, so the table is still out of sync. Based on learnings from the PR objective: this change adds 19 verifier tests covering primitive-operation obligations.

🤖 Prompt for 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.

In `@TESTING.md` around lines 61 - 62, The TESTING.md table entry for
`test_verifier.py` is out of sync with the actual test coverage added by this
PR. Update the line and test count numbers for `test_verifier.py` to reflect the
19 new primitive-operation obligation tests being added. Expand the description
to document the new div/mod-by-zero and array-bounds obligation cases, ensuring
the coverage list accurately reflects all obligation types now verified by the
test suite.
🤖 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.

Outside diff comments:
In `@vera/verifier.py`:
- Around line 3943-3949: The E526 division by zero error hint in the fix string
hard-codes `@Int.1` as the divisor, but the actual divisor could be `@Int.0`,
`@Nat`, or a compound expression, which misleads users about which De Bruijn
slot to guard. Locate where the fix hint is constructed for this error, capture
the actual divisor expression being used in the division operation, and
dynamically insert the rendered divisor into the hint message instead of
hard-coding `@Int.1`, ensuring the message references the correct divisor value.
- Around line 2040-2064: When scrutinee_z3 is None (meaning the scrutinee is
opaque and cannot be translated), the current code leaves arm_env as the outer
slot_env, which allows pattern bindings like Some(`@Int`) to incorrectly reference
outer parameters instead of the matched payload. To fix this, when scrutinee_z3
is None, create a fresh pattern environment for arm_env similar to how the
narrowing walker handles this case with _fresh_pattern_env(...). This ensures
pattern variables are properly isolated from the outer scope and verified
against the matched payload rather than the caller's precondition.
- Around line 1967-1984: The block walker does not maintain correct variable
scoping when translating let statement RHS values. When val is None
(untranslatable RHS), the code skips pushing a fresh shadow slot, causing later
expressions to incorrectly resolve to outer parameters instead of the current
scope. Additionally, the code completely ignores LetDestruct statements, missing
trapping operations in their RHS and any slots they introduce. Fix this by
ensuring that even when val is None, a shadow slot is still pushed for the let
binding in the cur_env to maintain correct scoping semantics. Also add handling
for ast.LetDestruct statements by walking their RHS for primitive op obligations
and pushing appropriate bindings to cur_env, similar to the LetStmt handling.

---

Duplicate comments:
In `@TESTING.md`:
- Around line 61-62: The TESTING.md table entry for `test_verifier.py` is out of
sync with the actual test coverage added by this PR. Update the line and test
count numbers for `test_verifier.py` to reflect the 19 new primitive-operation
obligation tests being added. Expand the description to document the new
div/mod-by-zero and array-bounds obligation cases, ensuring the coverage list
accurately reflects all obligation types now verified by the test suite.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 29446339-af9a-477b-8f6f-ed1665f62f0c

📥 Commits

Reviewing files that changed from the base of the PR and between 3848f0d and 4d9a7e4.

⛔ Files ignored due to path filters (2)
  • docs/llms-full.txt is excluded by !docs/**
  • tests/conformance/ch04_primitive_obligations.vera is excluded by !**/*.vera
📒 Files selected for processing (9)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_verifier.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
vera/verifier.py (2)

2040-2064: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Freshen match-arm bindings before discharging arm-body traps.

When scrutinee_z3 is None, arm_env remains the outer slot_env; a pattern such as Box(@int) then shadows an outer @Int at runtime, but the walker proves 1 / @Int.0`` against the outer slot. Also thread constructor source facts into arm assumptions so refined fields such as Box(NonZero) can discharge `@NonZero.0 != 0` instead of false-failing. This should mirror the match handling already used by `_walk_for_nat_binding_obligations`.

Suggested shape
                 if scrutinee_z3 is not None:
                     bound = smt._bind_pattern(
                         scrutinee_z3, arm.pattern, slot_env,
                     )
                     if bound is not None:
                         arm_env = bound
                     pat_cond = smt._pattern_condition(
                         scrutinee_z3, arm.pattern,
                     )
+                else:
+                    arm_env = self._fresh_pattern_env(
+                        arm.pattern, slot_env, smt,
+                    )
+                arm_assumptions = assumptions
+                if isinstance(arm.pattern, ast.ConstructorPattern):
+                    arm_assumptions = assumptions + self._subpattern_source_facts(
+                        expr.scrutinee, scrutinee_z3, arm.pattern, smt,
+                    )
                 if pat_cond is not None:
                     smt._path_conditions.append(pat_cond)
                     try:
                         self._walk_for_primitive_op_obligations(
-                            decl, arm.body, smt, arm_env, assumptions,
+                            decl, arm.body, smt, arm_env, arm_assumptions,
                         )
                     finally:
                         smt._path_conditions.pop()
                 else:
                     self._walk_for_primitive_op_obligations(
-                        decl, arm.body, smt, arm_env, assumptions,
+                        decl, arm.body, smt, arm_env, arm_assumptions,
                     )
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 2040 - 2064, When scrutinee_z3 is None, the
pattern bindings are not freshened, causing the walker to prove obligations
against the outer slot_env instead of the freshened bindings. Modify the
match-arm handling in the method to freshen pattern bindings even when
scrutinee_z3 is None by calling an appropriate freshening function on the
pattern, similar to how _walk_for_nat_binding_obligations handles match
expressions. Additionally, thread constructor source facts into the assumptions
parameter passed to _walk_for_primitive_op_obligations calls so that refined
field constraints like NonZero can be properly discharged during arm body
verification.

1967-1987: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Mirror block binding rebinding so later checks use the new slot.

For let @int= <untranslatable>; 1 /@Int.0``, val is None leaves `cur_env` pointing at any outer `@Int.0`, so the div-zero proof can discharge against the wrong variable. `LetDestruct` is also skipped entirely, so traps in the destructure RHS and later uses of destructured slots are missed. Mirror the nat-binding walker: walk `LetDestruct.value`, and push a fresh slot for every new binding when the exact RHS/component term is unavailable.

Localised direction
                     val = smt.translate_expr(stmt.value, cur_env)
+                    if val is None:
+                        val = self._fresh_slot_var(smt, stmt.type_expr)
                     if val is not None:
                         type_name = smt._type_expr_to_slot_name(stmt.type_expr)
                         if type_name is not None:
                             cur_env = cur_env.push(type_name, val)
+                elif isinstance(stmt, ast.LetDestruct):
+                    self._walk_for_primitive_op_obligations(
+                        decl, stmt.value, smt, cur_env, assumptions,
+                    )
+                    # Rebind destructured slots here as well, using exact
+                    # literal components when available and `_fresh_slot_var`
+                    # otherwise, so following expressions cannot resolve stale
+                    # outer bindings.
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 1967 - 1987, In the block statement processing
within the method handling ast.Block, fix the environment binding by ensuring
that when translating stmt.value in a LetStmt results in None (untranslatable),
still push a fresh slot binding to cur_env instead of skipping it, and add
missing handling for ast.LetDestruct statements by walking their value
expressions for primitive op obligations and pushing fresh slots for all
destructured bindings to mirror the nat-binding walker behavior, ensuring later
checks use the correct slot binding rather than falling back to outer scopes.
♻️ Duplicate comments (3)
FAQ.md (1)

53-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Narrow the auto-synthesis claim.

The opening sentence still reads as if any operand-dependent primitive gets an auto-synthesised obligation. This PR only covers the listed cases; keep that scope explicit, and call out the remaining Tier-3-only bodies from #779 as well.

Suggested wording
- It does — the verifier auto-synthesises a proof obligation at every primitive operation whose well-definedness depends on operand values, and discharges it from the surrounding preconditions and path conditions.
+ It does — the verifier auto-synthesises proof obligations for the primitive operations covered here, and discharges them from the surrounding preconditions and path conditions.

Based on learnings: #779 still leaves closure, quantifier, and handler bodies Tier-3.

🤖 Prompt for 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.

In `@FAQ.md` around lines 53 - 59, The opening sentence claims auto-synthesis
applies to "any primitive operation whose well-definedness depends on operand
values," but this PR only covers specific cases. Revise this sentence to
explicitly narrow the scope and list only the operations that are actually
auto-synthesized in this change (division/modulo, array indexing, and
subtraction/narrowing). Additionally, reference issue `#779` to clarify that
closure, quantifier, and handler bodies remain Tier-3-only and are not included
in this auto-synthesis coverage.
vera/verifier.py (2)

2104-2110: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not terminate on interpolation containers.

InterpolatedString is executable: "x=\(1 / @Int.0)" evaluates the division while constructing the string. The fallback currently treats it as a leaf, so E526/E527 sites inside interpolation remain silent runtime traps. Add the same ast.InterpolatedString branch used by the nat-binding walker. This is the remaining part of the earlier container-walking finding.

Proposed fix
+        if isinstance(expr, ast.InterpolatedString):
+            for part in expr.parts:
+                if isinstance(part, ast.Expr):
+                    self._walk_for_primitive_op_obligations(
+                        decl, part, smt, slot_env, assumptions,
+                    )
+            return
+
         # Remaining expression types terminate the walk.  Literals and slot
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 2104 - 2110, The current fallback logic in the
verifier treats InterpolatedString as a leaf node that terminates the walk, but
InterpolatedString is actually executable and contains operations that need
static checking (like the division in "x=\(1 / `@Int.0`)"). Add a new branch in
the walk logic to handle ast.InterpolatedString by walking into its contents,
similar to how the nat-binding walker handles InterpolatedString, so that
E526/E527 violations inside interpolations are properly detected instead of
remaining as silent runtime traps.

1339-1396: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Walk primitive operations in contract expressions before trusting them.

Phase 5.5 still only scans decl.body, so requires, ensures, and decreases can contain a / b or arr[i] without E526/E527. In particular, a requires(...) expression is translated and appended to assumptions before its own primitive traps are checked, so a trapping precondition can become a trusted SMT premise. Walk each contract expression at its discharge point, using only the assumptions valid before that clause is assumed; for ensures, set @result first, then walk the postcondition expression. This appears to be the same contract-walking gap flagged earlier, but it is still present in the current hunk.

Minimal shape of the fix
         for contract in decl.contracts:
             if isinstance(contract, ast.Requires):
                 self.summary.total += 1
+                self._walk_for_primitive_op_obligations(
+                    decl, contract.expr, smt, slot_env, assumptions,
+                )
                 if self._is_trivial(contract):
                     self.summary.tier1_verified += 1
                     self._record_obligation(
@@
                 # Translate the postcondition with `@T.result` → body result
                 smt.set_result_var(body_expr)
+                self._walk_for_primitive_op_obligations(
+                    decl, contract.expr, smt, slot_env, assumptions,
+                )
                 z3_post = smt.translate_expr(contract.expr, slot_env)
@@
         for contract in decl.contracts:
             if isinstance(contract, ast.Decreases):
                 self.summary.total += 1
+                for measure in contract.exprs:
+                    self._walk_for_primitive_op_obligations(
+                        decl, measure, smt, slot_env, assumptions,
+                    )
                 if self._verify_decreases(
                     decl, contract, smt, slot_env, group_decls,
                 ):

Also applies to: 1438-1474, 1588-1592

🤖 Prompt for 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.

In `@vera/verifier.py` around lines 1339 - 1396, The code currently only scans the
function body for primitive operation safety violations (E526/E527 for
underflow, division by zero, array bounds) via
_walk_for_primitive_op_obligations, but contract expressions (requires, ensures,
decreases) are translated and used as SMT assumptions without checking them
first. Walk each contract expression for primitive operation safety before it is
trusted as an assumption: in the requires loop, call
_walk_for_primitive_op_obligations on the contract expression before translating
it to SMT and adding to assumptions; for ensures contracts (around lines
1438-1474), walk them at their discharge point after setting `@result`; for
decreases contracts (around lines 1588-1592), walk them with the appropriate
assumption context. Use only the assumptions valid before each contract clause
is assumed.
🤖 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 `@CHANGELOG.md`:
- Around line 13-17: The CHANGELOG entry incorrectly states that an undecidable
divisor (one the solver cannot prove non-zero) becomes a compile error. Revise
the wording in the first paragraph to clarify that only provably zero divisors
are compile-time errors (E526), while undecidable divisor cases remain Tier-3
runtime-guarded instead of failing at compile time. Additionally, qualify or
adjust the "last Tier-0 silent failure" claim since closure/quantifier/handler
bodies are still tracked under issue `#779` and not fully addressed, so the
statement about closing the last Tier-0 silent failure should either be narrowed
in scope or removed.

In `@HISTORY.md`:
- Line 390: The release note for v0.0.177 in HISTORY.md claims to have closed
"the last Tier-0 silent failure" but this is inaccurate given that issue `#779`
remains open. Revise the description of the integer division/modulo by zero and
array index bounds fix to include a caveat similar to the one referenced in
`#779`, clarifying that the new obligations cover the remaining div/mod/index gaps
outside fresh-scope bodies rather than claiming all silent failures are now
eliminated. This ensures the release note accurately reflects the scope of the
fix without overstating its completeness.

In `@spec/06-contracts.md`:
- Around line 250-251: The specification text incorrectly states that index
operations inside closures or quantifier bodies are Tier-3 and counted in `vera
verify --json`, but the actual verifier implementation does not currently emit
these as obligations. Revise the section describing array indexing behavior to
accurately reflect the current implementation: clarify that index operations
within closures and quantifier bodies are not yet emitted as obligations,
removing or correcting the claim that they are counted in the JSON output,
ensuring the specification matches the actual verifier behavior demonstrated in
regression tests.

---

Outside diff comments:
In `@vera/verifier.py`:
- Around line 2040-2064: When scrutinee_z3 is None, the pattern bindings are not
freshened, causing the walker to prove obligations against the outer slot_env
instead of the freshened bindings. Modify the match-arm handling in the method
to freshen pattern bindings even when scrutinee_z3 is None by calling an
appropriate freshening function on the pattern, similar to how
_walk_for_nat_binding_obligations handles match expressions. Additionally,
thread constructor source facts into the assumptions parameter passed to
_walk_for_primitive_op_obligations calls so that refined field constraints like
NonZero can be properly discharged during arm body verification.
- Around line 1967-1987: In the block statement processing within the method
handling ast.Block, fix the environment binding by ensuring that when
translating stmt.value in a LetStmt results in None (untranslatable), still push
a fresh slot binding to cur_env instead of skipping it, and add missing handling
for ast.LetDestruct statements by walking their value expressions for primitive
op obligations and pushing fresh slots for all destructured bindings to mirror
the nat-binding walker behavior, ensuring later checks use the correct slot
binding rather than falling back to outer scopes.

---

Duplicate comments:
In `@FAQ.md`:
- Around line 53-59: The opening sentence claims auto-synthesis applies to "any
primitive operation whose well-definedness depends on operand values," but this
PR only covers specific cases. Revise this sentence to explicitly narrow the
scope and list only the operations that are actually auto-synthesized in this
change (division/modulo, array indexing, and subtraction/narrowing).
Additionally, reference issue `#779` to clarify that closure, quantifier, and
handler bodies remain Tier-3-only and are not included in this auto-synthesis
coverage.

In `@vera/verifier.py`:
- Around line 2104-2110: The current fallback logic in the verifier treats
InterpolatedString as a leaf node that terminates the walk, but
InterpolatedString is actually executable and contains operations that need
static checking (like the division in "x=\(1 / `@Int.0`)"). Add a new branch in
the walk logic to handle ast.InterpolatedString by walking into its contents,
similar to how the nat-binding walker handles InterpolatedString, so that
E526/E527 violations inside interpolations are properly detected instead of
remaining as silent runtime traps.
- Around line 1339-1396: The code currently only scans the function body for
primitive operation safety violations (E526/E527 for underflow, division by
zero, array bounds) via _walk_for_primitive_op_obligations, but contract
expressions (requires, ensures, decreases) are translated and used as SMT
assumptions without checking them first. Walk each contract expression for
primitive operation safety before it is trusted as an assumption: in the
requires loop, call _walk_for_primitive_op_obligations on the contract
expression before translating it to SMT and adding to assumptions; for ensures
contracts (around lines 1438-1474), walk them at their discharge point after
setting `@result`; for decreases contracts (around lines 1588-1592), walk them
with the appropriate assumption context. Use only the assumptions valid before
each contract clause is assumed.
🪄 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: 2abaa0d8-2b03-4368-b914-6a51d8f55962

📥 Commits

Reviewing files that changed from the base of the PR and between cf1ec81 and 4d9a7e4.

⛔ Files ignored due to path filters (7)
  • docs/SKILL.md is excluded by !docs/**
  • 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/ch04_primitive_obligations.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (19)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_spec_examples.py
  • spec/06-contracts.md
  • tests/conformance/manifest.json
  • tests/test_verifier.py
  • vera/__init__.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

Comment thread CHANGELOG.md Outdated
Comment thread HISTORY.md Outdated
Comment thread spec/06-contracts.md Outdated
aallan and others added 2 commits June 21, 2026 21:05
CodeRabbit round (7 inline + 2 outside-diff), verified each against
current code.

Fixed (still-valid):
- Walker now recurses into InterpolatedString parts: `"x: \(a/b)"`
  carried no E526 (confirmed reachable gap; the Nat-binding walker
  already handled this position). +1 test.
- FAQ: scope E526 to *integer* division and add the float-exempt note
  early.
- ROADMAP: narrow "every trapping primitive operation" to the specific
  ops now obligated; note float-to-int conversion stays runtime-guarded.
- TESTING: the test_verifier.py row now documents the #680 obligation
  tests.

Skipped (verified not still-valid):
- E527 "or string" title + index_bounds "string" comment -- already
  fixed in 4d9a7e4.
- Closure/quantifier-body indexes "should emit tier3" -- the spec does
  not require it; the walker deliberately does not recurse into closures
  (#779), and the test correctly pins that non-recursion.
- Walk contract clauses; match-scrutinee-None fresh env; block
  stale-binding + LetDestruct -- not demonstrably reachable as a wrong
  result for the div/index operation checks (operands are re-translated
  fresh per site; probes + the silent-failure-hunter audit found no
  false-Tier-1; the Nat-binding `_fresh_*` handling targets its narrowing
  check's value-flow bugs, a different mechanism). The walker-
  completeness family is tracked in #779.

Co-Authored-By: Claude <[email protected]>
- spec 6.4.3: an index inside a closure/quantifier body carries NO static
  obligation (the walker doesn't recurse into fresh-scope bodies) --
  corrected the overstatement that those are "counted" Tier-3; the codegen
  bounds-check covers them (walker coverage #779, Tier-1 proof #427).
- CHANGELOG: an unguarded divisor is a compile error when it has a zero
  counterexample; a genuinely undecidable divisor falls to Tier 3.

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 21, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Outside-diff findings, verified against current code:

  • Use fresh pattern bindings on opaque match scrutinee / invalidate shadowing block bindings: Skipping — verified these don't reproduce a wrong result for the div/index operation checks. Operands are re-translated fresh at each operation site; my probes (opaque-scrutinee match, shadowing let, LetDestruct) and the pr-review-toolkit silent-failure-hunter's adversarial audit found no false-Tier-1. The _fresh_slot_var / _fresh_pattern_env handling in the @Nat-binding walker targets that walker's narrowing value-flow bugs (feat: @Nat binding-site narrowing obligations (#552) #748 / release: v0.0.173 — @Nat narrowing at projection & instantiation sites (#747, #749) #756), a different mechanism. Folded into the walker-completeness follow-up Extend obligation walkers to closure / quantifier / handler-clause bodies #779 if a concrete repro surfaces.
  • Render the actual divisor in the E526 fix hint: Skipping for consistency — the sibling reports _report_underflow (E502) and _report_nat_binding (E503) use the same illustrative generic example rather than rendering the operand, and the diagnostic already points at the offending division. Rendering the operand expression would need a source pretty-printer the other reports don't use.

The InterpolatedString, test_verifier.py-row, and FAQ-scope outside-diff items were addressed in 7331b61; the spec closure-overstatement in 3fcb372.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan — understood and accepted on all counts.

Fresh pattern / shadowing bindings: The scoping argument is correct. The _fresh_slot_var / _fresh_pattern_env machinery specifically targets value-flow through the @Nat-narrowing walker's De Bruijn environment, not the division/index walkers where operands are re-translated at the operation site. With no concrete repro from the adversarial audit, deferring to #779 is the right call.

E526 fix-hint operand rendering: The consistency argument holds. Mixing operand-rendering into E526 while E502/E503 stay with generic illustrative examples would create a diagnostic style inconsistency, and it would require a source pretty-printer that isn't justified by this PR's scope.

7331b61 / 3fcb372: The InterpolatedString/test_verifier.py-row/FAQ-scope and spec-closure-overstatement items are confirmed addressed.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
FAQ.md (1)

53-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the Tier-3 escape hatch for opaque divisors.

The division sentence is still too absolute: the verifier only proves integer div/mod safety when it can translate the divisor; otherwise it records a Tier-3 runtime guard. Mention that fallback here so readers do not think every unguarded divisor is a compile error.

Suggested wording
- Integer division and modulo are decidable, so an unguarded divisor is always a compile error.
+ Integer division and modulo are Tier-1 decidable when the divisor can be translated to SMT; otherwise they fall back to a Tier-3 runtime-guarded check.
🤖 Prompt for 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.

In `@FAQ.md` around lines 53 - 59, The second paragraph statement "Integer
division and modulo are decidable, so an unguarded divisor is always a compile
error" is too absolute and doesn't account for opaque divisors. Revise this
sentence to clarify that divisors whose values can be determined through
preconditions or path conditions are statically proven and trigger compile error
E526 if unguarded, but when the divisor is opaque (similar to how array_length
is treated), the safety obligation falls to a Tier-3 runtime guard instead of a
static error. This aligns the divisor behavior with the array bounds explanation
already present in the paragraph.
vera/verifier.py (2)

2051-2065: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include failed earlier match arms in the arm path condition.

This only assumes the current pattern condition. For ordered matches, a wildcard/binding arm is also reached because all previous arms failed; without those negated conditions, a safe shape like match @Int.0{ 0 -> 0, _ -> 1 /@Int.0 } is checked without @Int.0 != 0 and can produce a false E526. Track prior translatable pattern conditions and push not previous && current for each arm.

🤖 Prompt for 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.

In `@vera/verifier.py` around lines 2051 - 2065, The match arm processing in the
loop around pat_cond only includes the current pattern condition in the path
conditions, but for ordered matches, later arms are only reached if all previous
arms failed. To fix this, maintain a collection of pattern conditions from
previously processed arms. For each arm being processed, construct the effective
path condition to include both the current pattern condition (pat_cond) AND the
negation of all accumulated previous pattern conditions. After processing each
arm's body in the _walk_for_primitive_op_obligations call, add the current arm's
translatable pattern condition to the collection of previous conditions so that
subsequent iterations correctly account for this arm's failure as a
precondition. This ensures the SMT solver receives the complete path condition
including why earlier arms were not taken, preventing false positives in safety
checks.

1970-1985: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Mirror binding semantics so obligations do not use stale slots.

The primitive-op walker does not handle LetDestruct at all, so a RHS like Tuple(@Int.0/@Int.1) can hide E526. It also leaves cur_env/arm_env unchanged when a let RHS or match scrutinee is untranslatable, so a later @Int.0 or @Array<T>.0 can resolve to a stale outer binding and be proved safe incorrectly. Mirror the nat-binding walker here: walk LetDestruct.value, shadow destructured slots, and use fresh slot/pattern bindings when translation fails.

Minimal direction for preserving shadowing
                 if isinstance(stmt, ast.LetStmt):
                     self._walk_for_primitive_op_obligations(
                         decl, stmt.value, smt, cur_env, assumptions,
                     )
                     val = smt.translate_expr(stmt.value, cur_env)
+                    type_name = smt._type_expr_to_slot_name(stmt.type_expr)
+                    if val is None and type_name is not None:
+                        val = self._fresh_slot_var(smt, stmt.type_expr)
+                        if val is None:
+                            stale = cur_env.resolve(type_name, 0)
+                            if stale is not None:
+                                val = z3.FreshConst(
+                                    stale.sort(), prefix="primlet",
+                                )
                     if val is not None:
-                        type_name = smt._type_expr_to_slot_name(stmt.type_expr)
                         if type_name is not None:
                             cur_env = cur_env.push(type_name, val)
+                elif isinstance(stmt, ast.LetDestruct):
+                    self._walk_for_primitive_op_obligations(
+                        decl, stmt.value, smt, cur_env, assumptions,
+                    )
+                    for te in stmt.type_bindings:
+                        type_name = smt._type_expr_to_slot_name(te)
+                        if type_name is None:
+                            continue
+                        fresh = self._fresh_slot_var(smt, te)
+                        if fresh is None:
+                            stale = cur_env.resolve(type_name, 0)
+                            if stale is not None:
+                                fresh = z3.FreshConst(
+                                    stale.sort(), prefix="primdestruct",
+                                )
+                        if fresh is not None:
+                            cur_env = cur_env.push(type_name, fresh)
@@
                 if scrutinee_z3 is not None:
                     bound = smt._bind_pattern(
                         scrutinee_z3, arm.pattern, slot_env,
                     )
                     if bound is not None:
                         arm_env = bound
                     pat_cond = smt._pattern_condition(
                         scrutinee_z3, arm.pattern,
                     )
+                else:
+                    arm_env = self._fresh_pattern_env(
+                        arm.pattern, slot_env, smt,
+                    )

Also applies to: 2041-2065

🤖 Prompt for 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.

In `@vera/verifier.py` around lines 1970 - 1985, The primitive-op walker is
missing handling for LetDestruct statements, which allows hidden unsafe
operations and stale slot references. Add an elif branch to handle
isinstance(stmt, ast.LetDestruct) after the ExprStmt case: walk stmt.value for
primitive-op obligations just like in LetStmt, translate the value using
smt.translate_expr, and shadow the destructured slots with fresh bindings in
cur_env by updating it to push the new bindings. Ensure that when the value
translation fails, you still create fresh slot/pattern bindings to prevent later
references from resolving to stale outer bindings, mirroring the behavior of the
nat-binding walker.
♻️ Duplicate comments (3)
CHANGELOG.md (1)

13-13: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

** Revert the misleading wording about undecidable divisors.**

This sentence still incorrectly implies undecidable divisors become compile errors. Per the verifier implementation (context snippet 3, _check_div_zero_obligation), only provably zero divisors trigger E526; undecidable cases degrade to Tier-3 runtime-guarded (else: ... tier3_runtime += 1).

The phrase "an unguarded divisor the solver can't prove non-zero is now a compile error" should be revised to clarify the discharge tiers:

Division and modulo are Tier-1-decidable: provably zero divisors fail as E526; 
undecidable divisors remain Tier-3 runtime-guarded (float division is exempt — 
`f64.div` by zero yields inf/NaN, not a trap).

Also, the claim "closing the last Tier-0 silent failure" on line 13–14 is too broad whilst issue #779 remains open for closure/quantifier/handler bodies. The entry itself acknowledges this scope limitation; the "last" framing should be qualified or removed to match that scoping.

🤖 Prompt for 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.

In `@CHANGELOG.md` at line 13, The CHANGELOG entry contains two instances of
misleading wording that need correction. First, the phrase about unguarded
divisors being compile errors should be revised to clarify that only provably
zero divisors trigger E526 compile errors, while undecidable divisors remain
Tier-3 runtime-guarded. Second, the claim of "closing the last Tier-0 silent
failure" is too broad given that issue `#779` remains open for
closure/quantifier/handler bodies, so this framing should be qualified or
removed to accurately reflect the current scope limitations acknowledged
elsewhere in the same entry. Update the division and modulo description to
accurately distinguish between provably zero cases (E526) and undecidable cases
(Tier-3), and adjust the scope language to match the acknowledged limitation
that some operation sites still fall outside this guarantee.
README.md (1)

50-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Scope the promise to the trapping primitives this PR covers.

This still reads as if every primitive operation gets an obligation. Float division is exempt, and the verifier only falls back to Tier 3 for cases it cannot translate or prove. Tighten the wording so the README matches the actual contract.

Suggested wording
- The compiler synthesises the same obligations for primitive operations themselves. Computing `@Int.1` / `@Int.0` without proving the divisor non-zero is now a compile error (E526), not a runtime trap; an array index is proved in bounds where the length is statically known and otherwise bounds-checked at runtime; `@Nat` subtraction underflow and `@Int` → `@Nat` narrowing are checked the same way. So `vera verify` clean means every primitive operation is either proved safe at compile time or honestly reported as a runtime-guarded check — no silent failures.
+ The compiler synthesises the same obligations for the trapping primitive operations covered here: integer division and modulo by zero, array indexing, `@Nat` subtraction underflow, and `@Int` → `@Nat` narrowing. Float division is exempt because it yields `inf`/`NaN`. So `vera verify` clean means those operations are either proved safe at compile time or honestly reported as runtime-guarded checks — no silent failures.
🤖 Prompt for 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.

In `@README.md` at line 50, The README text currently overstates the scope of the
compiler's obligation synthesis by implying that every primitive operation
receives the same treatment. Revise the paragraph starting with "The compiler
synthesises the same obligations..." to be more precise and scoped: explicitly
note which primitive operations are covered by this PR's obligations (integer
division, array indexing, subtraction, narrowing), clarify that float division
is exempt from these guarantees, and specify that the verifier only generates
obligations for cases it can translate and prove, falling back to Tier 3 runtime
checks for others. Ensure the revised wording accurately reflects the actual
contract rather than suggesting universal coverage.
vera/verifier.py (1)

1393-1396: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Scan primitive operations in contract expressions before trusting them.

Line 1394 still only walks decl.body, so requires, ensures, and decreases expressions can contain a / b or arr[i] without producing E526/E527. A requires(@Int.0/@Int.0 == 1) is especially dangerous: Z3 translates division as total, then this precondition is added to assumptions, while the runtime contract can still trap for 0. Walk each contract expression at its discharge site: requires before appending that precondition to assumptions, ensures after set_result_var(body_expr) and before translating/using the postcondition, and decreases inside or before _verify_decreases.

🤖 Prompt for 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.

In `@vera/verifier.py` around lines 1393 - 1396, The current code only walks
`decl.body` for primitive operation obligations, but contract expressions like
`decl.requires`, `decl.ensures`, and `decl.decreases` can contain unsafe
operations without being checked. Add calls to
`_walk_for_primitive_op_obligations` for each contract expression at the
appropriate discharge sites: walk `decl.requires` before it is added to
`assumptions`, walk `decl.ensures` after `set_result_var` is called and before
the postcondition is used, and walk `decl.decreases` within or before the
`_verify_decreases` method call to ensure all contract expressions are properly
validated for primitive operation safety.
🤖 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.

Outside diff comments:
In `@FAQ.md`:
- Around line 53-59: The second paragraph statement "Integer division and modulo
are decidable, so an unguarded divisor is always a compile error" is too
absolute and doesn't account for opaque divisors. Revise this sentence to
clarify that divisors whose values can be determined through preconditions or
path conditions are statically proven and trigger compile error E526 if
unguarded, but when the divisor is opaque (similar to how array_length is
treated), the safety obligation falls to a Tier-3 runtime guard instead of a
static error. This aligns the divisor behavior with the array bounds explanation
already present in the paragraph.

In `@vera/verifier.py`:
- Around line 2051-2065: The match arm processing in the loop around pat_cond
only includes the current pattern condition in the path conditions, but for
ordered matches, later arms are only reached if all previous arms failed. To fix
this, maintain a collection of pattern conditions from previously processed
arms. For each arm being processed, construct the effective path condition to
include both the current pattern condition (pat_cond) AND the negation of all
accumulated previous pattern conditions. After processing each arm's body in the
_walk_for_primitive_op_obligations call, add the current arm's translatable
pattern condition to the collection of previous conditions so that subsequent
iterations correctly account for this arm's failure as a precondition. This
ensures the SMT solver receives the complete path condition including why
earlier arms were not taken, preventing false positives in safety checks.
- Around line 1970-1985: The primitive-op walker is missing handling for
LetDestruct statements, which allows hidden unsafe operations and stale slot
references. Add an elif branch to handle isinstance(stmt, ast.LetDestruct) after
the ExprStmt case: walk stmt.value for primitive-op obligations just like in
LetStmt, translate the value using smt.translate_expr, and shadow the
destructured slots with fresh bindings in cur_env by updating it to push the new
bindings. Ensure that when the value translation fails, you still create fresh
slot/pattern bindings to prevent later references from resolving to stale outer
bindings, mirroring the behavior of the nat-binding walker.

---

Duplicate comments:
In `@CHANGELOG.md`:
- Line 13: The CHANGELOG entry contains two instances of misleading wording that
need correction. First, the phrase about unguarded divisors being compile errors
should be revised to clarify that only provably zero divisors trigger E526
compile errors, while undecidable divisors remain Tier-3 runtime-guarded.
Second, the claim of "closing the last Tier-0 silent failure" is too broad given
that issue `#779` remains open for closure/quantifier/handler bodies, so this
framing should be qualified or removed to accurately reflect the current scope
limitations acknowledged elsewhere in the same entry. Update the division and
modulo description to accurately distinguish between provably zero cases (E526)
and undecidable cases (Tier-3), and adjust the scope language to match the
acknowledged limitation that some operation sites still fall outside this
guarantee.

In `@README.md`:
- Line 50: The README text currently overstates the scope of the compiler's
obligation synthesis by implying that every primitive operation receives the
same treatment. Revise the paragraph starting with "The compiler synthesises the
same obligations..." to be more precise and scoped: explicitly note which
primitive operations are covered by this PR's obligations (integer division,
array indexing, subtraction, narrowing), clarify that float division is exempt
from these guarantees, and specify that the verifier only generates obligations
for cases it can translate and prove, falling back to Tier 3 runtime checks for
others. Ensure the revised wording accurately reflects the actual contract
rather than suggesting universal coverage.

In `@vera/verifier.py`:
- Around line 1393-1396: The current code only walks `decl.body` for primitive
operation obligations, but contract expressions like `decl.requires`,
`decl.ensures`, and `decl.decreases` can contain unsafe operations without being
checked. Add calls to `_walk_for_primitive_op_obligations` for each contract
expression at the appropriate discharge sites: walk `decl.requires` before it is
added to `assumptions`, walk `decl.ensures` after `set_result_var` is called and
before the postcondition is used, and walk `decl.decreases` within or before the
`_verify_decreases` method call to ensure all contract expressions are properly
validated for primitive operation safety.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9d0b78f3-5445-423c-8e42-3d0bd411a9d0

📥 Commits

Reviewing files that changed from the base of the PR and between 4d9a7e4 and 7331b61.

⛔ Files ignored due to path filters (1)
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (7)
  • CHANGELOG.md
  • FAQ.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_verifier.py
  • vera/verifier.py

…778)

CodeRabbit's recurring precision point: closure/quantifier/handler-body
ops are not statically obligated (the walker stops at fresh-scope bodies,
#779), so the unqualified "last Tier-0 silent failure" / "verify clean
then trapped" phrasing was imprecise.

- CHANGELOG/HISTORY: scope the close to "the last item in the Tier-0
  silent-failures section" and to a division/index *reported as proven*
  (closure-body ops are unobligated, not falsely-proven).
- ROADMAP/FAQ: note that closure/quantifier/handler-body ops stay
  runtime-guarded (loud at runtime, never a silent wrong value, #779).

Doc-only (the spec closure-counting + FAQ integer/float scope were
already fixed in 3fcb372 / 7331b61); validated doc-count, site-asset,
changelog, faq/readme-example, and limitation gates manually.

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 21, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Re-review round — mostly re-posts of resolved findings:

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

@aallan — confirmed on all points.

  • a145367 qualification: The scoping of "last Tier-0 silent failure" to the roadmap-section item and to proven division/index is the correct framing. Closure/quantifier/handler-body traps are loud at runtime (no silent wrong value), so "unobligated" is the precise description, not "silent failure" — the docs now reflect that distinction correctly and #779 tracks the lift.
  • Previously-fixed items (3fcb372, 7331b61, a145367): spec 6.4.3 closure overstatement, FAQ float/integer scope, InterpolatedString recursion, TESTING row — all confirmed addressed.
  • Outside-diff re-posts: The match-None / stale-binding / render-divisor items remain non-applicable for the operation walkers. Operands re-translated at each operation site, adversarial audit found no false-Tier-1, and the illustrative-hint style is consistent with E502/E503. No action needed; #779 is the right tracker for the walker-completeness family.

Nothing outstanding. The implementation is sound, documentation is accurate, and coverage gaps are closed. Approving.

✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

CodeRabbit outside-diff, verified against current code now with valid
LetDestruct syntax: a division/index inside a `let Tuple<...> = Tuple(a /
b, ...)` destructure value was completely missed (no obligation) -- the
block walker skipped LetDestruct statements. It now walks the destructure
value and rebinds each destructured slot with a fresh var (Tier-3) so a
later op doesn't resolve to a stale outer binding; the LetStmt path
mirrors the Nat-binding walker by pushing a fresh var when the RHS is
untranslatable. +1 test; zero corpus regression, tier ledger unchanged.

Doc accuracy:
- FAQ: an opaque divisor (untranslatable) falls to Tier-3, not always a
  compile error.
- README: scope the "proved safe / runtime-guarded" claim -- closure-body
  ops and opaque divisors are runtime-guarded, not universally proven.

Skipped (verified not still-valid): match-scrutinee-None fresh env (fires
E526 correctly -- scrutinee translates, pattern bound), match-arm negated-
prior-pattern accumulation (parity with the Nat-binding walker, false-
positive-only, contrived), walk-contract-clauses (#779-family). Re-posted
CHANGELOG-divisor and last-silent-failure items were already addressed.

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 21, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai This round, verified against current code:

Fixed

  • LetDestruct values not walked (77d90fb): with valid syntax (let Tuple<@Int, @Int> = Tuple(@Int.0 / @Int.1, ...)) this is reachable — the division in the destructure value was missed entirely. The block walker now walks the destructure value and rebinds each destructured slot with a fresh var (so a later op falls to Tier-3, not a stale outer binding); the LetStmt path also pushes a fresh var on an untranslatable RHS, mirroring the Nat-binding walker. +1 test, zero corpus regression.
  • FAQ "unguarded divisor is always a compile error": corrected — an opaque (untranslatable) divisor falls to Tier-3, like array bounds.
  • README scope: the "proved safe / runtime-guarded" line now scopes to what the verifier reaches (opaque divisors and closure-body ops are runtime-guarded, not universally proven).

Skipped (verified not still-valid)

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

@aallan — confirmed on all points.

LetDestruct fix (77d90fb): The gap was genuine. A division or modulo inside a destructure RHS (let Tuple<@int, @int> = Tuple(@Int.0/@Int.1, ...)) was silently skipped by the walker — a real E526 miss. Walking the destructure value and issuing a fresh Z3 var per slot is the correct fix: it surfaces the operation at the RHS, and ensures any subsequent operation involving a destructured slot degrades to Tier-3 rather than inheriting a stale outer binding. Mirroring the Nat-binding walker's LetStmt fresh-var-on-untranslatable-RHS is the right consistency call.

FAQ / README scope corrections: Both fixes are accurate — an opaque (untranslatable) divisor genuinely falls to Tier-3, and the README's "proved safe / runtime-guarded" claim should be scoped to verifier-reachable positions. These were documentation overclaims worth correcting.

Skipped items:

  • Match-scrutinee-None: If the scrutinee translates (which it does in the probed case), the pattern binding is live and E526 fires correctly. Non-applicable for div/index.
  • Match-arm negated-prior accumulation: Parity with the mature Nat-binding walker is the right consistency baseline; the false-positive-only nature and the contrived setup required make this low-priority, and #779 is the right tracker if a concrete repro surfaces.
  • Contract clause walking: Correctly scoped to #779requires/ensures/decreases are not function-body positions and the obligation model for contracts is a separate concern from #680's body-obligation focus.

Nothing outstanding from this review. Implementation is sound, documentation is accurate, coverage gaps are closed or tracked. Approving.

✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

@aallan

aallan commented Jun 21, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vera/verifier.py (1)

2057-2086: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Carry constructor-field source facts into primitive checks.

The match walker binds constructor patterns but does not add the source facts that _walk_for_nat_binding_obligations() already derives. For Option<{ @int|@Int.0 != 0 }> matched as Some(@nonZero), a division by the bound field can false-fail E526 because the arm assumptions lack the field’s source refinement.

🐛 Proposed fix
             for arm in expr.arms:
                 arm_env = slot_env
                 pat_cond = None
+                arm_assumptions = assumptions
                 if scrutinee_z3 is not None:
                     bound = smt._bind_pattern(
                         scrutinee_z3, arm.pattern, slot_env,
                     )
@@
                     pat_cond = smt._pattern_condition(
                         scrutinee_z3, arm.pattern,
                     )
+                    if isinstance(arm.pattern, ast.ConstructorPattern):
+                        arm_assumptions = assumptions + self._subpattern_source_facts(
+                            expr.scrutinee, scrutinee_z3, arm.pattern, smt,
+                        )
                 if pat_cond is not None:
                     smt._path_conditions.append(pat_cond)
                     try:
                         self._walk_for_primitive_op_obligations(
-                            decl, arm.body, smt, arm_env, assumptions,
+                            decl, arm.body, smt, arm_env, arm_assumptions,
                         )
                     finally:
                         smt._path_conditions.pop()
                 else:
                     self._walk_for_primitive_op_obligations(
-                        decl, arm.body, smt, arm_env, assumptions,
+                        decl, arm.body, smt, arm_env, arm_assumptions,
                     )
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 2057 - 2086, When processing match expressions
(ast.MatchExpr), the pattern binding logic in the arm loop does not carry
constructor-field source facts from the patterns into the arm assumptions,
causing false E526 failures. After successfully binding a pattern with
smt._bind_pattern() in the arm processing loop, extract the source refinement
facts from the matched constructor's fields and add them to the assumptions
passed to the recursive _walk_for_primitive_op_obligations call for the arm
body. This ensures that field constraints (like non-zero refinements) are
available when checking primitive operations in the matched arm, similar to how
_walk_for_nat_binding_obligations() handles them.
🤖 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 50: The current text describing array index handling only mentions the
runtime-guarded path for cases where bounds cannot be proven. Add a mention of
the E527 compile-time error that occurs when the verifier can prove an index is
provably out of range. In the section discussing array index bounds checking
(where it currently describes proving indices in bounds and runtime
bounds-checking for dynamic lengths), include that the verifier also raises E527
as a compile-time error when it can prove an index access is out of bounds,
making this a loud failure mode distinct from the runtime guard fallback.

In `@vera/verifier.py`:
- Around line 1975-1984: The issue is that when `_fresh_slot_var()` returns None
for Array and ADT type bindings, the code fails to shadow stale outer slots,
allowing later operations to resolve to incorrect outer bindings. Modify the
logic where `_fresh_slot_var()` is called (around line 1980 and in the other
location at lines 2949-2979) to check if the returned value is None. When None
is returned for untranslatable Array or ADT bindings, create a FreshConst with
the same sort as the stale term from the outer environment and use that
FreshConst to shadow the stale binding by pushing it to cur_env with the
appropriate type_name, instead of leaving the binding unpushed.
- Around line 1985-1999: The current implementation unconditionally rebinds
every destructured component to a fresh variable in the LetDestruct handler,
which discards literal value information and makes safe operations appear
unsafe. Instead of always using fresh variables, extract the actual destructured
component terms from stmt.value (such as tuple fields or constructor arguments),
attempt to translate these components, and only create fresh variables for
components that are opaque or cannot be translated. Modify the logic around
_fresh_slot_var and the cur_env.push call to use the original translated
component term when available before falling back to the fresh variable as a
last resort.

---

Outside diff comments:
In `@vera/verifier.py`:
- Around line 2057-2086: When processing match expressions (ast.MatchExpr), the
pattern binding logic in the arm loop does not carry constructor-field source
facts from the patterns into the arm assumptions, causing false E526 failures.
After successfully binding a pattern with smt._bind_pattern() in the arm
processing loop, extract the source refinement facts from the matched
constructor's fields and add them to the assumptions passed to the recursive
_walk_for_primitive_op_obligations call for the arm body. This ensures that
field constraints (like non-zero refinements) are available when checking
primitive operations in the matched arm, similar to how
_walk_for_nat_binding_obligations() handles them.
🪄 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: 9239841a-4123-4e5a-a491-3e359525d032

📥 Commits

Reviewing files that changed from the base of the PR and between cf1ec81 and 77d90fb.

⛔ Files ignored due to path filters (7)
  • docs/SKILL.md is excluded by !docs/**
  • 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/ch04_primitive_obligations.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (19)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_spec_examples.py
  • spec/06-contracts.md
  • tests/conformance/manifest.json
  • tests/test_verifier.py
  • vera/__init__.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

Comment thread README.md Outdated
Comment thread vera/verifier.py Outdated
Comment thread vera/verifier.py Outdated
…E E527 (PR #778)

CodeRabbit found a regression I introduced in 77d90fb: rebinding a
destructured slot to a fresh unconstrained var made a SAFE destructured
divisor a false E526 -- `Tuple(10, 5)` then dividing by the second
component fired E526, because a fresh Int slot has no non-zero invariant
(the Nat-binding walker's fresh-var trick relies on Nat's `>= 0`, which
doesn't transfer to the divisor check). Reverted the fresh-var pushes
(LetStmt + LetDestruct); kept the destructure-value walk that catches a
division INSIDE the value. A later op on a destructured slot falls to
Tier-3. +1 regression test.

Also: README now mentions the E527 compile-time error for a provably
out-of-bounds index, not just the runtime-guard fallback.

Skipped (verified): a refined-field divisor in a match arm discharges
correctly (no false E526); the Array/ADT fresh-slot-None case is Tier-3
(no false discharge), parity with the Nat-binding walker.

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 21, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai This round — and thank you for the persistence, finding 3 caught a real regression I'd introduced:

Fixed (25b9e07)

  • Destructured fresh-var rebind → false E526 (your finding): in 77d90fb I rebound each destructured slot to a fresh unconstrained var. That made a safe destructured divisor a false positive — let Tuple<@Int, @Int> = Tuple(10, 5); @Int.0 / @Int.1 fired E526, because a fresh @Int has no != 0 invariant (the @Nat-binding walker's fresh-var trick works only because @Nat carries >= 0, which doesn't transfer to the divisor check). Reverted the fresh-var pushes (LetStmt + LetDestruct); kept the destructure-value walk that catches a division inside the value. A later op on a destructured slot now falls to Tier-3. +1 regression test.
  • README now names the E527 compile-time error for a provably out-of-bounds index, not just the runtime-guard fallback.

Skipped (verified against current code)

  • Refined-field divisor in a match arm: probed match … { Some(@NonZero) -> @Int.0 / @NonZero.0 } — discharges at Tier 1 (no false E526). The refinement fact is carried for a translatable scrutinee.
  • Array/ADT _fresh_slot_var returning None: with the rebind reverted, a destructured Array/ADT slot is simply not rebound → a later index falls to Tier-3, no stale-outer false discharge in probes. Same _fresh_slot_var scalar-only behavior the mature @Nat-binding walker uses; enhancing it (literal-component projection) is Extend obligation walkers to closure / quantifier / handler-clause bodies #779-family.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan — the regression root-cause analysis is exactly right, and the fix in 25b9e07 is the minimal correct change.

On the false-E526 regression: the @Nat-binding walker's fresh-var push is sound precisely because it immediately follows with assert fresh_var >= 0, reflecting the @Nat type invariant. There is no analogous universally-true constraint for a divisor slot — a fresh, unconstrained @Int is zero-able by definition — so rebinding stripped the concrete value knowledge the prover needed, making every subsequent division on a destructured slot look unguarded. Keeping the destructure-value walk (catches a division inside the RHS) while dropping the slot rebind (preserves the prover's knowledge of the concrete bound value) is the correct minimal fix. Tier-3 fallback for later ops on the unbound slots is the right conservative outcome.

On the skipped items: both are correctly classified. Refined-field divisors in a match arm discharge at Tier-1 because the refinement fact is live for a translatable scrutinee — no gap there. The Array/ADT _fresh_slot_var-returns-None path is parity-consistent with the mature @Nat-binding walker and the Tier-3 fallback matches the expected conservative behavior; literal-component projection for structured slots remains a #779-family item.

README E527 naming: naming the E527 compile-time error path (not just the runtime-guard fallback) closes the documentation asymmetry with E526 — correct fix.

Implementation is sound, the regression is closed, and the documentation is accurate. Nothing outstanding.

@aallan

aallan commented Jun 21, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@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

♻️ Duplicate comments (1)
README.md (1)

50-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Narrow the divisor failure mode.

This still reads as though every unproven divisor becomes E526. The verifier only emits E526 when it can prove the divisor is zero; opaque or untranslatable divisors stay Tier 3.

♻️ Suggested wording
- The compiler synthesises the same obligations for primitive operations themselves.  Computing `@Int.1 / `@Int.0`` without proving the divisor non-zero is now a compile error (E526), not a runtime trap; an array index is proved in bounds where the length is statically known, a compile error (E527) where provably out of bounds, and otherwise bounds-checked at runtime; `@Nat` subtraction underflow and `@Int` → `@Nat` narrowing are checked the same way.  So a division or array index that `vera verify` reports as proven is safe for all inputs; where it can't prove one — an opaque divisor, a dynamic array length, or an op inside a closure body — the runtime guard catches it rather than silently producing a wrong value.  (Float division is exempt: divide-by-zero yields inf/NaN, not a trap.)
+ The compiler synthesises the same obligations for primitive operations themselves.  Computing `@Int.1 / `@Int.0`` raises E526 when the verifier can prove the divisor is zero; opaque or untranslatable divisors remain Tier 3 and are checked at runtime.  An array index is proved in bounds where the length is statically known, a compile error (E527) where provably out of bounds, and otherwise bounds-checked at runtime; `@Nat` subtraction underflow and `@Int` → `@Nat` narrowing are checked the same way.  So a division or array index that `vera verify` reports as proven is safe for all inputs; where it can't prove one — an opaque divisor, a dynamic array length, or an op inside a closure body — the runtime guard catches it rather than silently producing a wrong value.  (Float division is exempt: divide-by-zero yields inf/NaN, not a trap.)
🤖 Prompt for 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.

In `@README.md` at line 50, The current wording in the divisor failure mode
section incorrectly implies that all unproven divisors result in compile error
E526. Clarify that E526 is only emitted when the verifier can prove the divisor
is actually zero; for opaque or untranslatable divisors where proof is not
possible, they remain Tier 3 status instead. Revise the explanation to clearly
distinguish between divisors that are provably zero (E526 error) and divisors
that cannot be analyzed (Tier 3, which gets runtime checking).
🤖 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 `@SKILL.md`:
- Line 2276: The sentence in SKILL.md describing the conformance tests as
self-contained is inaccurate because it fails to mention the cross-module
dependency exception. Update the text to acknowledge that while the 92 programs
are generally self-contained, there is one exception:
ch07_cross_module_contracts.vera depends on
ch07_cross_module_contracts_lib.vera. Qualify the statement to reflect this
reality rather than claiming all programs import nothing.

In `@vera/verifier.py`:
- Around line 1975-1993: Destructured and pattern-matched bindings are not being
shadowed, allowing later operations to incorrectly resolve to outer bindings and
reuse their verification facts. In the LetDestruct case (starting at line 1980)
and similar pattern matching scenarios that also apply to lines 2055-2079, after
walking the value for primitive operations, you need to: (1) extract and rebind
translatable destructured/pattern components to their projected terms by pushing
them to cur_env, and (2) for opaque or untranslatable new bindings, create
shadow bindings in cur_env that mask the outer slots to force downstream
primitive-op sites to Tier 3 instead of reusing stale outer binding facts.
- Around line 3969-3975: The error fix messages for E526 and E527 use hard-coded
De Bruijn indices like `@Int.1` which may not match the actual divisor or operand
being checked. Replace the hard-coded variable references in the fix strings at
lines 3969-3975 and 4011-4018 with calls to ast.format_expr(...) to render the
actual divisor, collection, and index expressions dynamically. Additionally,
ensure that for E527 with `@Nat` indices, both the lower and upper bounds are
included in the suggested precondition or guard, not just the upper bound, to
provide complete and accurate guidance to users.

---

Duplicate comments:
In `@README.md`:
- Line 50: The current wording in the divisor failure mode section incorrectly
implies that all unproven divisors result in compile error E526. Clarify that
E526 is only emitted when the verifier can prove the divisor is actually zero;
for opaque or untranslatable divisors where proof is not possible, they remain
Tier 3 status instead. Revise the explanation to clearly distinguish between
divisors that are provably zero (E526 error) and divisors that cannot be
analyzed (Tier 3, which gets runtime checking).
🪄 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: 7625a261-45a3-4cb2-a3f8-caa64e4240a6

📥 Commits

Reviewing files that changed from the base of the PR and between cf1ec81 and 25b9e07.

⛔ Files ignored due to path filters (7)
  • docs/SKILL.md is excluded by !docs/**
  • 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/ch04_primitive_obligations.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (19)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_spec_examples.py
  • spec/06-contracts.md
  • tests/conformance/manifest.json
  • tests/test_verifier.py
  • vera/__init__.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

Comment thread SKILL.md Outdated
Comment thread vera/verifier.py
Comment thread vera/verifier.py Outdated
)

Surfaced by the #680 shadow/projection audit: a 14-mutation sweep found 8
green-for-the-wrong-reason tests in a 57-test battery. Broadened #387 from
"mutmut on checker/verifier" to the full suite + 92 conformance programs
(conformance first) and promoted it from the Ongoing-threads CI list into
Tier 1 (the safety-net tier, next to #392's smt.py soundness audit). Filed
#784 first, then found #387 already covered it — closed #784 as a duplicate
and folded the broadened scope + #680 provenance into #387.
@aallan

aallan commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

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

🤖 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 `@spec/06-contracts.md`:
- Line 250: In the paragraph about array indexing behavior, the sentence
beginning with "An index inside a closure or quantifier body carries no static
obligation at all" currently mentions only closures and quantifiers, but omits
handler-clause bodies which the walker also skips. Add handler-clause bodies to
this list of bodies that the walker deliberately excludes, so the sentence
accurately reflects that the verifier skips closure, quantifier, and
handler-clause bodies when walking the tree.

In `@vera/verifier.py`:
- Around line 2972-2992: The Real sort exemption check (checking if
divisor.sort() is not IntSort) at the end of the block needs to be moved earlier
in the logic. Currently it executes after recording Tier-3 obligations for
untranslatable divisors and opaque shadows, which means Float64/Real divisors
still get recorded as obligations despite not trapping on zero. Move the Real
sort check to execute immediately after translating the divisor and before any
obligation recording happens, so that float divisors are exempted without
generating bogus div_zero obligations.
🪄 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: 3f8e409a-3bd4-40f8-8a48-af2be2a8c150

📥 Commits

Reviewing files that changed from the base of the PR and between cf1ec81 and d6217b2.

⛔ Files ignored due to path filters (7)
  • docs/SKILL.md is excluded by !docs/**
  • 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/ch04_primitive_obligations.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (20)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_spec_examples.py
  • spec/06-contracts.md
  • tests/conformance/manifest.json
  • tests/test_verifier.py
  • vera/__init__.py
  • vera/ast.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

Comment thread spec/06-contracts.md Outdated
Comment thread vera/verifier.py
…ording (PR #778)

Two Minor CodeRabbit findings on the d6217b2 head:

- verifier.py div check: an untranslatable or destructured (shadow) @float64
  divisor recorded a bogus div_zero Tier-3 obligation, because the None /
  opaque-shadow recordings fire BEFORE the Real-sort exemption (which needs a
  translated term to call .sort()). Fix: exempt float divisors up front by the
  divisor's resolved TYPE (_resolved_type_of + _is_float64_type), before any
  recording. Kept the early _is_opaque_shadow fast path (it avoids two solver
  calls for direct int shadows; the float exit now precedes it).

- spec section 6.4.3: the "index inside a closure or quantifier body" sentence
  omitted handler-clause bodies, which the walker also skips (#779). Added it.

Tests (test-first, RED->GREEN): test_float64_shadow_divisor_records_no_obligation
(was a bogus div_zero tier3, now no obligation). The example tier ledger
(test_overall_tier_counts) drops accordingly: json's `/` divisor is @float64, so
it is now exempt rather than a bogus tier3 -- 263/32/295/0 -> 263/31/294/0.

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

…d section (PR #778)

ROADMAP is forward-facing only. Replaced the closed-out "Tier 0 -- Close the
silent failures -- done" section (a backwards-facing block recapping the
closure) with a single intro sentence: Tier 0 is done, no known silent failure
remains, see HISTORY.md -- then straight into Tier 1. No Tier 0 section.
@aallan

aallan commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vera/verifier.py (1)

2142-2172: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Thread constructor sub-pattern source facts into primitive-op arm checks.

This arm walk binds projected fields into arm_env, but still passes the original assumptions. For a scrutinee with a refined field such as Box(NonZero), Box(@nonZero) -> 1 / @NonZero.0`` checks E526 against an unconstrained accessor instead of the field’s != 0 source fact, so safe code can be rejected.

🐛 Proposed fix
             for arm in expr.arms:
                 arm_env = slot_env
                 pat_cond = None
+                arm_assumptions = assumptions
                 if scrutinee_z3 is not None:
                     bound = smt._bind_pattern(
                         scrutinee_z3, arm.pattern, slot_env,
                     )
                     if bound is not None:
                         arm_env = bound
                     pat_cond = smt._pattern_condition(
                         scrutinee_z3, arm.pattern,
                     )
+                    if isinstance(arm.pattern, ast.ConstructorPattern):
+                        arm_assumptions = assumptions + self._subpattern_source_facts(
+                            expr.scrutinee, scrutinee_z3, arm.pattern, smt,
+                        )
                 else:
                     # Untranslatable scrutinee (e.g. an effect op): the arm
                     # still binds pattern slots, so shadow them as TRACKED
                     # opaque consts.
                     arm_env = self._fresh_pattern_env(
                         arm.pattern, slot_env, smt, track=True,
                     )
                 if pat_cond is not None:
                     smt._path_conditions.append(pat_cond)
                     try:
                         self._walk_for_primitive_op_obligations(
-                            decl, arm.body, smt, arm_env, assumptions,
+                            decl, arm.body, smt, arm_env, arm_assumptions,
                         )
                     finally:
                         smt._path_conditions.pop()
                 else:
                     self._walk_for_primitive_op_obligations(
-                        decl, arm.body, smt, arm_env, assumptions,
+                        decl, arm.body, smt, arm_env, arm_assumptions,
                     )
🤖 Prompt for 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.

In `@vera/verifier.py` around lines 2142 - 2172, The
`_walk_for_primitive_op_obligations` calls for pattern match arms do not include
source facts from the refined pattern sub-types in their assumptions parameter,
causing safe field accesses to be incorrectly rejected. Update both calls to
`_walk_for_primitive_op_obligations` (the one inside the try/finally block when
pat_cond is not None, and the one in the else branch) to construct and pass
augmented assumptions that include the source facts and constraints derived from
the pattern refinements in arm.pattern, so that field operations like division
checks on refined fields (e.g. Box(NonZero).0) can use the inferred non-zero
constraint.
♻️ Duplicate comments (1)
AGENTS.md (1)

11-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Qualify the “self-contained” claim.

ch07_cross_module_contracts.vera imports ch07_cross_module_contracts_lib.vera, so the suite is not entirely standalone.

🤖 Prompt for 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.

In `@AGENTS.md` at line 11, The description of the conformance suite in the
documentation claims all programs are "self-contained," but this is not entirely
accurate since ch07_cross_module_contracts.vera depends on
ch07_cross_module_contracts_lib.vera. Modify the text to acknowledge that while
most conformance programs are self-contained, some programs span multiple files
and include cross-module imports as part of their test structure, making them
not entirely standalone examples.
🤖 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.

Outside diff comments:
In `@vera/verifier.py`:
- Around line 2142-2172: The `_walk_for_primitive_op_obligations` calls for
pattern match arms do not include source facts from the refined pattern
sub-types in their assumptions parameter, causing safe field accesses to be
incorrectly rejected. Update both calls to `_walk_for_primitive_op_obligations`
(the one inside the try/finally block when pat_cond is not None, and the one in
the else branch) to construct and pass augmented assumptions that include the
source facts and constraints derived from the pattern refinements in
arm.pattern, so that field operations like division checks on refined fields
(e.g. Box(NonZero).0) can use the inferred non-zero constraint.

---

Duplicate comments:
In `@AGENTS.md`:
- Line 11: The description of the conformance suite in the documentation claims
all programs are "self-contained," but this is not entirely accurate since
ch07_cross_module_contracts.vera depends on
ch07_cross_module_contracts_lib.vera. Modify the text to acknowledge that while
most conformance programs are self-contained, some programs span multiple files
and include cross-module imports as part of their test structure, making them
not entirely standalone examples.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a7857bd4-0e18-44e4-a068-5fd44d89abee

📥 Commits

Reviewing files that changed from the base of the PR and between cf1ec81 and dfeaefe.

⛔ Files ignored due to path filters (7)
  • docs/SKILL.md is excluded by !docs/**
  • 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/ch04_primitive_obligations.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (20)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_spec_examples.py
  • spec/06-contracts.md
  • tests/conformance/manifest.json
  • tests/test_verifier.py
  • vera/__init__.py
  • vera/ast.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

CodeRabbit duplicate finding (AGENTS.md:11, the same caveat fixed in SKILL.md):
the conformance suite is not entirely self-contained -- ch07_cross_module_contracts.vera
imports its ch07_cross_module_contracts_lib.vera companion. Qualified the
"self-contained" claim; regenerated docs/llms-full.txt (AGENTS.md feeds the bundle).

The outside-diff finding (match-arm pattern source facts in primitive checks)
does NOT reproduce and needs no change: the #746 _subpattern_source_facts hook
already carries a matched constructor field's refinement into the #680 division
check. Custom-ADT differential confirms -- Box(NonZero).0 verifies at Tier 1,
unrefined Box(Int).0 fires E526. Same disposition as the earlier 2057-2086.
@aallan

aallan commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Dispositions for this review:

AGENTS.md:11 (duplicate — conformance "self-contained") — fixed in a331416. Qualified the claim: the suite is mostly self-contained, with ch07_cross_module_contracts.vera the exception (it imports its ch07_cross_module_contracts_lib.vera companion). Matches the SKILL.md caveat; regenerated docs/llms-full.txt (AGENTS.md feeds the bundle).

Match-arm pattern source facts in primitive checks (vera/verifier.py ~2142-2172, outside-diff) — does not reproduce; no change needed. The #746 _subpattern_source_facts hook already carries a matched constructor field's refinement into the #680 primitive check (a global SMT channel, not the assumptions list). Confirmed with a custom-ADT differential:

  • data Box { Box(NonZero) }match ... { Box(@NonZero) -> 100 / @NonZero.0 } verifies at Tier 1;
  • unrefined data Box { Box(Int) }100 / @Int.0 fires E526.

So the refinement reaches the division check and the discharge is genuine (the unrefined case still errors) — threading the same facts through assumptions would be redundant. Same disposition as the earlier 2057-2086 outside-diff finding.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

@aallan — confirmed on both dispositions.

a331416 (AGENTS.md:11 self-contained qualifier): The qualification is precise — "mostly self-contained" with the ch07_cross_module_contracts.vera / ch07_cross_module_contracts_lib.vera pair as the explicit exception. Parity with the SKILL.md caveat is the right consistency target; the regenerated docs/llms-full.txt bundle will carry the corrected claim.

Match-arm pattern source facts (~2142-2172, outside-diff): The differential is the right proof. _subpattern_source_facts (#746) propagates the matched constructor field's refinement through the global SMT channel — that channel is already live when _check_div_zero_obligation runs, so:

  • Box(@nonZero) -> 100 / @NonZero.0`` → Tier-1 discharge (refinement fact in scope).
  • Unrefined Box(@int) -> 100 / @Int.0`` → E526 (no != 0 constraint, correctly flagged).

Threading the same facts through assumptions would duplicate them across two channels with no soundness benefit. Same disposition as the 2057-2086 finding — non-applicable, no change needed.

Triggering full review on head.

✅ Action performed

Full review finished.

@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 `@README.md`:
- Line 50: The terminology in the explanation of divisor safety is inaccurate.
The phrase describing an opaque divisor as "stays Tier 3, bounds-checked at
runtime" conflates two different safety mechanisms. Revise this section to
clarify that an opaque or untranslatable divisor remains Tier 3 behind a runtime
zero-divisor guard (not a bounds check). Reserve the term "bounds-checked"
exclusively for array index operations, making the distinction between runtime
guards and bounds checks clear and accurate throughout the explanation.
🪄 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: 5a1a6b9c-7159-4aa9-aeaa-80c911df34d0

📥 Commits

Reviewing files that changed from the base of the PR and between cf1ec81 and fb089e7.

⛔ Files ignored due to path filters (7)
  • docs/SKILL.md is excluded by !docs/**
  • 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/ch04_primitive_obligations.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (20)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_spec_examples.py
  • spec/06-contracts.md
  • tests/conformance/manifest.json
  • tests/test_verifier.py
  • vera/__init__.py
  • vera/ast.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

Comment thread README.md Outdated
…bounds-checked" (PR #778)

CodeRabbit Minor (README:50): an opaque divisor's runtime guard is the
zero-divisor (divide_by_zero) trap, not a bounds check -- "bounds-checked" is the
array-index term. Changed the divisor phrase to "guarded at runtime by the
zero-divisor trap"; left the array-index "bounds-checked at runtime" intact.
@aallan

aallan commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

…opped — 700ff2d got 0 workflow runs; no code change, squash-merge cleans this)

Co-Authored-By: Claude <[email protected]>
@aallan

aallan commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@aallan

aallan commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai resume

@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 `@scripts/check_spec_examples.py`:
- Line 145: In the ALLOWLIST entry for the SafeDiv fragment in the
06-contracts.md file, update the line number from 344 to 345. The current entry
has the tuple ("06-contracts.md", 344) with the comment about the SafeDiv type
definition, but line 344 is the opening code fence and the actual fragment
content is on line 345. Change the second element of this tuple from 344 to 345
to correctly reference where the SafeDiv type definition actually appears in the
spec.
🪄 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: 298d505f-c85d-425c-97a2-0e465f25a1eb

📥 Commits

Reviewing files that changed from the base of the PR and between cf1ec81 and 9db0226.

⛔ Files ignored due to path filters (7)
  • docs/SKILL.md is excluded by !docs/**
  • 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/ch04_primitive_obligations.vera is excluded by !**/*.vera
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (20)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_spec_examples.py
  • spec/06-contracts.md
  • tests/conformance/manifest.json
  • tests/test_verifier.py
  • vera/__init__.py
  • vera/ast.py
  • vera/errors.py
  • vera/obligations/core.py
  • vera/verifier.py

Comment thread scripts/check_spec_examples.py
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

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.

Auto-inject obligations for primitive operations (division, modulo, array index) to match README's static-safety claim

1 participant