Skip to content

fix(verifier): ADT equality over Float64 fields models per-field fpEQ, not structural datatype = (#871)#879

Merged
aallan merged 1 commit into
mainfrom
fix/871-nan-datatype-eq
Jul 3, 2026
Merged

fix(verifier): ADT equality over Float64 fields models per-field fpEQ, not structural datatype = (#871)#879
aallan merged 1 commit into
mainfrom
fix/871-nan-datatype-eq

Conversation

@aallan

@aallan aallan commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the gravest bug class in the tracker: a false Tier-1. Z3's structural datatype = treats a NaN Float64 field as self-equal, while the runtime compares fields with f64.eq (NaN ≠ NaN) — so vera verify proved 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 containing Float64 now expand to same-constructor recognizers plus fieldwise fpEQ (_datatype_value_eq + _sort_contains_fp). A recursive Float64-containing ADT has no finite expansion → honest, loud Tier-3 demotion (E522 + tier3_runtime count). 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.eq canonical; the SMT model now matches it.

Evidence

  • RED (ensures + vera-run differential, the repo's soundness-probe standard): on main, the refl(MkW(nan())) probe reports 4 verified (Tier 1) and exits 0, then vera run exits 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.
  • Orchestrator flip probe (independent): main = false Tier-1 + runtime violation; branch = vera verify exit 1, E500 naming the counterexample @W.0 = MkW(NaN).
  • Mutation kills (4, pycache-purged): decomposition gate bypassed → 5 RED; fpEQ leaf degraded to = → 4 RED; _sort_contains_fp forced False → 5 RED; NEQ arm unrouted → exactly the NEQ test RED.
  • No over-demotion: per-file verify --json tier 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

    • Equality on datatypes containing Float64 now matches runtime behaviour, including NaN and signed zero cases.
    • Recursive datatypes with Float64 fields now fall back to a runtime check instead of being incorrectly proven earlier.
  • Tests

    • Added regression coverage for Float64-aware datatype equality, nested cases, and recursive behaviour.
    • Updated the reported test totals in the project documentation.
  • Documentation

    • Clarified contract and equality behaviour for floating-point values and datatypes in the specification and changelog.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 70e45403-ceba-471a-9b4f-d8482b8dd215

📥 Commits

Reviewing files that changed from the base of the PR and between 6abf7f5 and cd34f3b.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • spec/06-contracts.md
  • tests/test_adt_float64_eq_871.py
  • vera/smt.py
📝 Walkthrough

Walkthrough

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

Changes

Float64 ADT equality fix

Layer / File(s) Summary
SMT datatype equality translation
vera/smt.py
_translate_binary now routes datatype-sorted ==/!= through new _datatype_value_eq, which decomposes equality per constructor/field using fpEQ for FP fields, recurses into nested FP-containing datatypes, and returns None for recursive datatypes to trigger Tier-3 fallback; a new _sort_contains_fp helper detects FP fields transitively.
Regression tests for #871
tests/test_adt_float64_eq_871.py
New test module with _verify/_ensures helpers and cases for NaN reflexivity/inequality, signed-zero equality, nested ADT decomposition, recursive Tier-3 demotion, NaN-guarded Tier-1 preservation, and non-Float64 ADT structural equality.
Documentation, changelog and tracking updates
CHANGELOG.md, KNOWN_ISSUES.md, ROADMAP.md, TESTING.md, spec/06-contracts.md
Documents the fix and recursive-ADT Tier-3 fallback, removes the resolved #871 known issue while adding #881/#882, updates test counts, and expands the contracts spec's Float64 equality rules.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: compiler, tests, spec, docs

🚥 Pre-merge checks | ✅ 8
✅ Passed checks (8 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix for ADT equality on Float64 fields.
Linked Issues check ✅ Passed The SMT change, recursive Tier-3 fallback, and regression tests address #871’s soundness bug and runtime mismatch requirements.
Out of Scope Changes check ✅ Passed The changes are limited to the verifier fix and closely related documentation, tests, and issue tracking updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Changelog Covers Public-Surface Changes ✅ Passed The only public-surface file changed is spec/06-contracts.md, and CHANGELOG.md explicitly documents the Float64-ADT equality decomposition and recursive Tier-3 fallback.
Spec And Implementation Move Together ✅ Passed vera/smt.py ADT equality semantics changed, and spec/06-contracts.md was updated to match nested Float64 decomposition and recursive Tier-3 fallback.
Diagnostics Carry An Error Code ✅ Passed The new verifier paths use registered codes (E522/E523), and every Diagnostic emission in verifier.py supplies an error_code; no changed diagnostic is code-less.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/871-nan-datatype-eq

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

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.01%. Comparing base (e1a71e3) to head (cd34f3b).

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              
Flag Coverage Δ
javascript 75.01% <ø> (ø)
python 95.00% <100.00%> (+<0.01%) ⬆️

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

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

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

@aallan

aallan commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@tests/test_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

📥 Commits

Reviewing files that changed from the base of the PR and between c0af1b9 and 6abf7f5.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • spec/06-contracts.md
  • tests/test_adt_float64_eq_871.py
  • vera/smt.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

Comment thread tests/test_adt_float64_eq_871.py
aallan pushed a commit that referenced this pull request Jul 3, 2026
…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]>
@aallan aallan force-pushed the fix/871-nan-datatype-eq branch from ce84b7a to cd34f3b Compare July 3, 2026 02:59
@aallan

aallan commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@aallan aallan merged commit 3e19e6b into main Jul 3, 2026
26 checks passed
@aallan aallan deleted the fix/871-nan-datatype-eq branch July 3, 2026 03:07
aallan pushed a commit that referenced this pull request Jul 3, 2026
…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]>
aallan pushed a commit that referenced this pull request Jul 3, 2026
…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]>
aallan added a commit that referenced this pull request Jul 3, 2026
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False Tier-1: Z3 datatype equality treats a NaN Float64 field as self-equal while runtime f64.eq disagrees

1 participant