fix(verifier): ADT equality over Float64 fields models per-field fpEQ, not structural datatype = (#871)#879
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 5 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughFixes a soundness bug where Z3 datatype equality on ADTs containing Float64 fields diverged from runtime f64.eq semantics (notably for NaN and signed zero). Equality now decomposes per-field using fpEQ, recursing through nested ADTs, and falls back to Tier-3 for recursive Float64-containing ADTs. Adds regression tests and updates documentation. ChangesFloat64 ADT equality fix
Estimated code review effort: 3 (Moderate) | ~25 minutes Suggested labels: 🚥 Pre-merge checks | ✅ 8✅ Passed checks (8 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #879 +/- ##
==========================================
+ Coverage 93.00% 93.01% +0.01%
==========================================
Files 95 95
Lines 29188 29233 +45
Branches 360 360
==========================================
+ Hits 27145 27190 +45
Misses 2037 2037
Partials 6 6
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:
|
…ings) Co-Authored-By: Claude <[email protected]>
|
@coderabbitai review |
✅ Action performedReview 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 `@tests/test_adt_float64_eq_871.py`:
- Around line 43-227: The FP-ADT tests still miss the multi-constructor
decomposition path in _datatype_value_eq, so add a sum-type case (for example a
datatype like M with both an FP constructor and a non-FP constructor) to
exercise the z3.Or(*arms) branch and the recognizer guards. Verify that
cross-constructor equality like an FP constructor instance versus the other
constructor stays disproved, and that same-constructor reflexivity with NaN
excluded remains Tier-1. Also add a recursive != case to cover the eq is None
path in the recursive handling, since the current recursive test only uses ==
and does not hit that branch.
🪄 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 Plus
Run ID: cb1802c3-da09-4dcc-89e8-d660ec7b9606
📒 Files selected for processing (7)
CHANGELOG.mdKNOWN_ISSUES.mdROADMAP.mdTESTING.mdspec/06-contracts.mdtests/test_adt_float64_eq_871.pyvera/smt.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
aallan/vera-bench(manual)
…ecursive-NEQ demotion (PR #879 review) Every FP ADT in the #871 battery had exactly one FP-carrying constructor, so the multi-arm z3.Or decomposition in _datatype_value_eq -- and its per-arm recognizer conjuncts -- was unexercised, and the NEQ arm's recursive-demotion path (eq is None -> None) was unpinned. Two new pins: a mixed-constructor sum type (MA(Float64) | MB(Int)) whose cross-constructor inequality must stay a Tier-1 proof (dropping the recognizer conjuncts flips exactly this test RED -- the unguarded wrong-constructor arm's unconstrained accessors lose the proof) plus NaN-guarded same-constructor reflexivity staying Tier-1; and != on a recursive FP-containing ADT demoting to an honest Tier-3 (RED against the structural-= fallback, which falsely proves it). Mutation-validated: recognizer-guard drop -> only the sum-type pin RED (8 others green); structural-= fallback -> 6 RED including the new recursive-NEQ pin. Skip-changelog: tests-only follow-up; the existing #871 [Unreleased] bullet was extended in this commit to note the fifth mutation axis Co-Authored-By: Claude <[email protected]>
…, not structural datatype = (#871) Per-field decomposition with same-constructor recognizers; recursive Float64-containing ADTs demote loudly to Tier-3 (E522). Includes the review round's multi-constructor and recursive-NEQ pins and the #881/#882 intake rows. Co-Authored-By: Claude <[email protected]>
ce84b7a to
cd34f3b
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
…d arguments (#882) A call to a helper whose requires() constrains ADT-typed parameters produced no call_pre obligation at all — no E501, no Tier-3 warning — so `vera verify` reported ok:true for a call it never statically examined, while the identical Int-parameter program correctly fired E501 and `vera run` trapped at runtime. Root cause in SmtContext._translate_call_with_info: a constructor-call argument (MkP(1)) only translates once the callee's concrete ADT sort exists in the Z3 sort cache, but in a caller context that never declared that ADT the sort was never materialised, so _find_sort_for_ctor returned None, the whole call bailed, and the obligation vanished. The call translator now materialises each ADT-typed parameter's sort from the callee's declared types before checking the precondition, so — post-#879 — MkP(1) vs MkP(2) is statically refutable (E501 + counterexample), a satisfiable call discharges, and nested-ADT / Float64-field-NaN arguments refute correctly. Where the argument or the precondition genuinely can't be modelled (a host-handle field such as Map, an undecidable predicate like string_length(...) > 0) the obligation demotes to a loud Tier-3 E522 warning rather than silently not existing (DESIGN.md: degrade loudly; the runtime guard still enforces it). Return-value modelling is deliberately unchanged: an ADT-argument call whose result feeds a postcondition stays opaque exactly as before, so no corpus program's postcondition tier moves. A before/after verify --json census over all 145 conformance + example programs shows tier1 unchanged (1231); the only movement is three example call sites (http, inference, async_http_fanout) gaining one honest Tier-3 call_pre demotion each for their undecidable string_length preconditions (+3 Tier-3). Adds a run-level conformance program (ch06_requires_adt_arg) and eight unit tests (TestAdtCallSitePrecondition). Mutation-validated three ways. Co-Authored-By: Claude <[email protected]>
…e group (#881) Close a completeness gap in the #881 mutually-recursive sort fix found by the PR #879 adversarial soundness review: a `data` cycle whose cross-reference passes THROUGH a `Tuple` field still crashed `vera verify` with the identical uncaught RecursionError #881 exists to eliminate — `data C { MkC(Tuple<C, Int>) }`, or a pair `data A { MkA(Tuple<B, Int>) }` / `data B { MkB(A) }`. `_collect_adt_group` discovered the back-referenced member through the Tuple's type args, but the Tuple sort was built by a FRESH `_get_or_create_adt_sort` call that did not share the group's builder map, so its ADT component re-entered sort creation for the still-uncached member and recursed unboundedly (C -> build Tuple<C,Int> -> build C -> build Tuple<C,Int> -> ...). A Tuple reachable through a member's field is now itself a group member — a single `Tuple` constructor whose fields are its concrete components — so its back-reference stitches to the shared in-progress builder and the whole strongly-connected group is declared together via `z3.CreateDatatypes`. The group representation is unified to `key -> [(ctor_name, concrete_field_types)]` so registered ADTs and Tuples build through one loop. The `_z3_sort_name` naming choke point is untouched (kept disjoint from the concurrent #884 sort-naming change). Audit (skeptic-flagged): Tuple is the only structural field-type carrier — an alias resolving to such a Tuple was affected too (aliases resolve to the Tuple before the verifier) and is now fixed; a refinement-wrapped ADT field was already unwrapped by `_enqueue_adt_types` and was never affected. Equality obligations mirror #871: a Tuple-mediated NON-FP cycle proves reflexivity Tier-1 (structural `=` agrees with the runtime); a recursive Float64-through-Tuple cycle has no finite equality expansion and demotes to a loud Tier-3 runtime check rather than a false proof; a FINITE Float64-in-Tuple field uses per-field fpEQ statically (correctly disproving reflexivity via a NaN counterexample). New conformance program `ch02_adt_tuple_recursive` (run level) and four regression tests + two mutation-kill parametrize cases in `tests/test_mutual_recursive_sorts_881.py`. Written test-first: each new case raises RecursionError on the pre-fix walk; mutation-validated — disabling the Tuple-member branch flips exactly the six Tuple-mediated tests back to RecursionError while the direct-reference tests stay green. A before/after `verify --json` census over all conformance + example programs shows zero tier movement beyond the new program (+3 Tier-1, +4 Tier-3, the same E525/E522 classes as the sibling mutual-recursion program). Co-Authored-By: Claude <[email protected]>
…crash Z3 construction (#881) (#894) * fix(smt): build mutually-recursive ADT sorts together via z3.CreateDatatypes (#881) A mutually-recursive `data` pair — `data A { MkA, WrapB(B) }` / `data B { MkB, WrapA(A) }` with a function over `@A` — passed `vera check` and ran under `vera run`, but `vera verify` aborted with an uncaught Python `RecursionError` deep in `_get_or_create_adt_sort`: a check-green program yielding a raw interpreter traceback (the same class as #861 and the #871 sort-equality lineage). The self-recursive case was handled by a single `self_ref_key`/`self_ref_dt` pair, but that pair could name only ONE in-progress sort, so building `A` recursed into building `B`, which recursed into building `A` again, until the recursion limit tripped (FP-independent — an Int-only pair crashed identically). Sort construction now declares every datatype reachable through constructor fields together as one strongly-connected group via `z3.CreateDatatypes` (`_collect_adt_group` + a builder map in `_vera_type_to_z3_sort`), the sound, complete option that keeps Tier-1 static reasoning (DESIGN.md: maximise static guarantees). Self-recursion is the singleton-group case; the `key -> z3.Datatype` name transformation is hoisted to one `_z3_sort_name` choke point. Because the sort now BUILDS, the #871 Float64-cycle guard is reachable for mutually-recursive types: a non-Float64 mutual pair proves reflexivity Tier-1, while a recursive Float64-containing mutual pair demotes to an honest loud Tier-3 runtime check rather than a false proof. New conformance program `ch02_adt_mutual_recursive` (run level) and regression suite `tests/test_mutual_recursive_sorts_881.py` (issue repro, 3-cycle, one-base-case pair, Float64-field pair, non-FP-Tier-1 vs recursive-FP-Tier-3 equality; mutation-kill re-raises RecursionError when group construction is reverted). `verify --json` census over all 37 examples shows zero tier movement (no corpus program declared mutually-recursive `data` before this). Co-Authored-By: Claude <[email protected]> * fix(smt): declare Tuple-mediated recursive datatype cycles in the same group (#881) Close a completeness gap in the #881 mutually-recursive sort fix found by the PR #879 adversarial soundness review: a `data` cycle whose cross-reference passes THROUGH a `Tuple` field still crashed `vera verify` with the identical uncaught RecursionError #881 exists to eliminate — `data C { MkC(Tuple<C, Int>) }`, or a pair `data A { MkA(Tuple<B, Int>) }` / `data B { MkB(A) }`. `_collect_adt_group` discovered the back-referenced member through the Tuple's type args, but the Tuple sort was built by a FRESH `_get_or_create_adt_sort` call that did not share the group's builder map, so its ADT component re-entered sort creation for the still-uncached member and recursed unboundedly (C -> build Tuple<C,Int> -> build C -> build Tuple<C,Int> -> ...). A Tuple reachable through a member's field is now itself a group member — a single `Tuple` constructor whose fields are its concrete components — so its back-reference stitches to the shared in-progress builder and the whole strongly-connected group is declared together via `z3.CreateDatatypes`. The group representation is unified to `key -> [(ctor_name, concrete_field_types)]` so registered ADTs and Tuples build through one loop. The `_z3_sort_name` naming choke point is untouched (kept disjoint from the concurrent #884 sort-naming change). Audit (skeptic-flagged): Tuple is the only structural field-type carrier — an alias resolving to such a Tuple was affected too (aliases resolve to the Tuple before the verifier) and is now fixed; a refinement-wrapped ADT field was already unwrapped by `_enqueue_adt_types` and was never affected. Equality obligations mirror #871: a Tuple-mediated NON-FP cycle proves reflexivity Tier-1 (structural `=` agrees with the runtime); a recursive Float64-through-Tuple cycle has no finite equality expansion and demotes to a loud Tier-3 runtime check rather than a false proof; a FINITE Float64-in-Tuple field uses per-field fpEQ statically (correctly disproving reflexivity via a NaN counterexample). New conformance program `ch02_adt_tuple_recursive` (run level) and four regression tests + two mutation-kill parametrize cases in `tests/test_mutual_recursive_sorts_881.py`. Written test-first: each new case raises RecursionError on the pre-fix walk; mutation-validated — disabling the Tuple-member branch flips exactly the six Tuple-mediated tests back to RecursionError while the direct-reference tests stay green. A before/after `verify --json` census over all conformance + example programs shows zero tier movement beyond the new program (+3 Tier-1, +4 Tier-3, the same E525/E522 classes as the sibling mutual-recursion program). Co-Authored-By: Claude <[email protected]> * docs(testing): sync check_conformance.py table rows to 111 conformance entries (#894 review) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: T <[email protected]> Co-authored-by: Claude <[email protected]>
Summary
Closes the gravest bug class in the tracker: a false Tier-1. Z3's structural datatype
=treats a NaNFloat64field as self-equal, while the runtime compares fields withf64.eq(NaN ≠ NaN) — sovera verifyproved postconditions the runtime then violated. The inverse also held:MkW(-0.0) == MkW(+0.0)produced a false E500 counterexample against a contract the runtime satisfies.The fix (vera/smt.py)
==/!=on a datatype sort transitively containingFloat64now expand to same-constructor recognizers plus fieldwisefpEQ(_datatype_value_eq+_sort_contains_fp). A recursive Float64-containing ADT has no finite expansion → honest, loud Tier-3 demotion (E522 +tier3_runtimecount). Non-FP datatype equality keeps structural=.Principle (DESIGN.md verification row — maximise static guarantees, degrade gracefully where SMT is undecidable; soundness over completeness): Tier-1 is kept exactly where a sound finite model exists, demoted exactly where it doesn't. Spec §9.8.2 + the #870 codegen make per-field
f64.eqcanonical; the SMT model now matches it.Evidence
refl(MkW(nan()))probe reports4 verified (Tier 1)and exits 0, thenvera runexits 1 with a postcondition violation. 5 of 7 new tests RED on main for exactly this reason, including a recursive float ADT and the −0.0/+0.0 inverse.vera verifyexit 1, E500 naming the counterexample@W.0 = MkW(NaN).fpEQleaf degraded to=→ 4 RED;_sort_contains_fpforced False → 5 RED; NEQ arm unrouted → exactly the NEQ test RED.verify --jsontier census across all 145 conformance+example files — zero deltas (tier1=1231, tier3=495 both sides).Docs
spec §6.3.1 documents the decomposition + recursive-ADT Tier-3 rule; CHANGELOG
[Unreleased]; KNOWN_ISSUES #871 row removed; TESTING/ROADMAP counts. No version bump (rides the v0.1.0 burndown).Closes #871
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Float64now matches runtime behaviour, includingNaNand signed zero cases.Float64fields now fall back to a runtime check instead of being incorrectly proven earlier.Tests
Float64-aware datatype equality, nested cases, and recursive behaviour.Documentation