feat(680): auto-synthesise division/modulo + array-index safety obligations (close last Tier-0 silent failure)#778
Conversation
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 Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
📝 WalkthroughWalkthroughImplements 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 ChangesPrimitive Operation Obligation Synthesis
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 winUse fresh pattern bindings when the match scrutinee is opaque.
When
scrutinee_z3isNone,arm_envremainsslot_env. A branch likeSome(@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 winInvalidate shadowing block bindings before checking later primitive ops.
Line 1967 only pushes a
letbinding when its RHS translates. If the RHS is untranslatable, later@Int.0/@Array<T>.0references in the block resolve to an outer binding, so an outerrequires(@Int.0!= 0)can incorrectly discharge10 /@Int.0`` afterlet@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
⛔ Files ignored due to path filters (7)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch04_primitive_obligations.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (18)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdHISTORY.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/06-contracts.mdtests/conformance/manifest.jsontests/test_verifier.pyvera/__init__.pyvera/errors.pyvera/obligations/core.pyvera/verifier.py
…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]>
pr-review-toolkit review (4 agents) — addressed in 4d9a7e4Ran 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 Test strength (pr-test-analyzer, two criticality-8 gaps)
Accuracy (comment-analyzer + code-reviewer)
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 CI green; all gates pass. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
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 winRender 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 winFresh-shadow match bindings when the scrutinee is opaque.
If
smt.translate_expr(expr.scrutinee, slot_env)returnsNone,arm_envremains the outer environment. A pattern binding such asSome(@int)can then leave@Int.0in the arm body pointing at an outer parameter, so1 /@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 liftMirror block binding semantics before checking later primitive ops.
When a
letRHS is untranslatable, this walker does not push a fresh shadow slot, so a later1 /@Int.0`` can resolve to an outer parameter and discharge from the wrongrequires. 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 winAdd 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
⛔ Files ignored due to path filters (2)
docs/llms-full.txtis excluded by!docs/**tests/conformance/ch04_primitive_obligations.verais excluded by!**/*.vera
📒 Files selected for processing (9)
CHANGELOG.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdTESTING.mdtests/test_verifier.pyvera/errors.pyvera/obligations/core.pyvera/verifier.py
There was a problem hiding this comment.
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 winFreshen match-arm bindings before discharging arm-body traps.
When
scrutinee_z3 is None,arm_envremains the outerslot_env; a pattern such asBox(@int)then shadows an outer@Intat runtime, but the walker proves1 /@Int.0`` against the outer slot. Also thread constructor source facts into arm assumptions so refined fields such asBox(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 winMirror block binding rebinding so later checks use the new slot.
For
let@int= <untranslatable>; 1 /@Int.0``,val is Noneleaves `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 winNarrow 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
#779as 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:
#779still 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 winDo not terminate on interpolation containers.
InterpolatedStringis 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 sameast.InterpolatedStringbranch 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 winWalk primitive operations in contract expressions before trusting them.
Phase 5.5 still only scans
decl.body, sorequires,ensures, anddecreasescan containa / borarr[i]without E526/E527. In particular, arequires(...)expression is translated and appended toassumptionsbefore 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; forensures, set@resultfirst, 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
⛔ Files ignored due to path filters (7)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch04_primitive_obligations.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (19)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/06-contracts.mdtests/conformance/manifest.jsontests/test_verifier.pyvera/__init__.pyvera/errors.pyvera/obligations/core.pyvera/verifier.py
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]>
|
@coderabbitai Outside-diff findings, verified against current code:
The InterpolatedString, |
|
Tip For best results, initiate chat on the files or code changes.
Fresh pattern / shadowing bindings: The scoping argument is correct. The 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/ |
There was a problem hiding this comment.
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 winAdd 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 winInclude 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 != 0and can produce a false E526. Track prior translatable pattern conditions and pushnot previous && currentfor 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 winMirror binding semantics so obligations do not use stale slots.
The primitive-op walker does not handle
LetDestructat all, so a RHS likeTuple(@Int.0/@Int.1)can hide E526. It also leavescur_env/arm_envunchanged when a let RHS or match scrutinee is untranslatable, so a later@Int.0or@Array<T>.0can resolve to a stale outer binding and be proved safe incorrectly. Mirror the nat-binding walker here: walkLetDestruct.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
#779remains 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 winScope 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 winScan primitive operations in contract expressions before trusting them.
Line 1394 still only walks
decl.body, sorequires,ensures, anddecreasesexpressions can containa / borarr[i]without producing E526/E527. Arequires(@Int.0/@Int.0== 1)is especially dangerous: Z3 translates division as total, then this precondition is added toassumptions, while the runtime contract can still trap for0. Walk each contract expression at its discharge site:requiresbefore appending that precondition toassumptions,ensuresafterset_result_var(body_expr)and before translating/using the postcondition, anddecreasesinside 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
⛔ Files ignored due to path filters (1)
docs/llms-full.txtis excluded by!docs/**
📒 Files selected for processing (7)
CHANGELOG.mdFAQ.mdREADME.mdROADMAP.mdTESTING.mdtests/test_verifier.pyvera/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]>
|
@coderabbitai Re-review round — mostly re-posts of resolved findings:
|
|
Nothing outstanding. The implementation is sound, documentation is accurate, and coverage gaps are closed. Approving. ✅ Action performedComments resolved. Approval is disabled; enable |
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]>
|
@coderabbitai This round, verified against current code: Fixed
Skipped (verified not still-valid)
|
|
LetDestruct fix (77d90fb): The gap was genuine. A division or modulo inside a destructure RHS ( 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:
Nothing outstanding from this review. Implementation is sound, documentation is accurate, coverage gaps are closed or tracked. Approving. ✅ Action performedComments resolved. Approval is disabled; enable |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
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 winCarry 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. ForOption<{@int|@Int.0!= 0 }>matched asSome(@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
⛔ Files ignored due to path filters (7)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch04_primitive_obligations.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (19)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/06-contracts.mdtests/conformance/manifest.jsontests/test_verifier.pyvera/__init__.pyvera/errors.pyvera/obligations/core.pyvera/verifier.py
…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]>
|
@coderabbitai This round — and thank you for the persistence, finding 3 caught a real regression I'd introduced: Fixed (25b9e07)
Skipped (verified against current code)
|
|
Tip For best results, initiate chat on the files or code changes.
On the false-E526 regression: the 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 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. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
README.md (1)
50-50:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winNarrow 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
⛔ Files ignored due to path filters (7)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch04_primitive_obligations.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (19)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/06-contracts.mdtests/conformance/manifest.jsontests/test_verifier.pyvera/__init__.pyvera/errors.pyvera/obligations/core.pyvera/verifier.py
) 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.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (7)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch04_primitive_obligations.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (20)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/06-contracts.mdtests/conformance/manifest.jsontests/test_verifier.pyvera/__init__.pyvera/ast.pyvera/errors.pyvera/obligations/core.pyvera/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]>
|
@coderabbitai full review |
✅ Action performedFull 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.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
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 winThread constructor sub-pattern source facts into primitive-op arm checks.
This arm walk binds projected fields into
arm_env, but still passes the originalassumptions. For a scrutinee with a refined field such asBox(NonZero),Box(@nonZero) -> 1 /@NonZero.0`` checks E526 against an unconstrained accessor instead of the field’s!= 0source 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 winQualify the “self-contained” claim.
ch07_cross_module_contracts.veraimportsch07_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
⛔ Files ignored due to path filters (7)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch04_primitive_obligations.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (20)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/06-contracts.mdtests/conformance/manifest.jsontests/test_verifier.pyvera/__init__.pyvera/ast.pyvera/errors.pyvera/obligations/core.pyvera/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.
|
@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 Match-arm pattern source facts in primitive checks (
So the refinement reaches the division check and the discharge is genuine (the unrefined case still errors) — threading the same facts through @coderabbitai full review |
|
a331416 (AGENTS.md:11 self-contained qualifier): The qualification is precise — "mostly self-contained" with the Match-arm pattern source facts (~2142-2172, outside-diff): The differential is the right proof.
Threading the same facts through Triggering full review on head. ✅ Action performedFull review finished. |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (7)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch04_primitive_obligations.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (20)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/06-contracts.mdtests/conformance/manifest.jsontests/test_verifier.pyvera/__init__.pyvera/ast.pyvera/errors.pyvera/obligations/core.pyvera/verifier.py
…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.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
…opped — 700ff2d got 0 workflow runs; no code change, squash-merge cleans this) Co-Authored-By: Claude <[email protected]>
|
@coderabbitai full review |
|
@coderabbitai resume |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (7)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch04_primitive_obligations.verais excluded by!**/*.verauv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (20)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdHISTORY.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlscripts/check_spec_examples.pyspec/06-contracts.mdtests/conformance/manifest.jsontests/test_verifier.pyvera/__init__.pyvera/ast.pyvera/errors.pyvera/obligations/core.pyvera/verifier.py
✅ Action performedReviews resumed. |
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 verifyclean ⟹ holds at runtime for these ops too.What changed
a / b,a % b(Int/Nat)b != 0f64.divby zero yields inf/NaN, not a trap)arr[i](Array<T>)0 <= i < array_length(arr)[1, 2, 3][5]); else honest Tier-3 (runtime-guarded)These join the existing
@Natsubtraction (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), butarray_lengthis 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
@Natsubtraction walker generalises to one_walk_for_primitive_op_obligationspass (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).divide_by_zero/out_of_boundsruntime traps already existed; this adds only the static layer.ObligationKindsdiv_zero/index_bounds.Tests & docs
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).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
Documentation
Tests
Chores