Prove behavioral equivalence between an original function and an AI-rewritten one within bounds, or return the concrete input that breaks it.
Project description
Congruent
Given an original function and an AI-rewritten one, prove behavioral equivalence within bounds, return the concrete input that breaks it — or admit it doesn't know.
Coding agents refactor, migrate, and "optimize" code constantly. The honest answer to "did this preserve behavior?" is usually tests plus vibes. Congruent gives a real answer for a deliberately narrow slice of the problem: EQUIVALENT up to bound N, COUNTEREXAMPLE: <concrete input>, or an honest UNKNOWN — never a guess dressed up as a proof.
It's equivalence checking (the EDA/formal-methods kind) pointed at the problem of trusting AI-generated code.
Scope (read this first)
The credibility of this tool is its honesty about what it does and doesn't do. v1 is intentionally small.
| In scope (v1) | Out of scope (v1 — see ROADMAP.md) |
|---|---|
| Pure, deterministic functions (no I/O, no global mutation) | Side effects, I/O, concurrency |
| Bounded inputs: machine ints, bools, fixed-length lists, bounded strings | Floating-point exactness |
| Integer/boolean arithmetic, comparisons, branches | Recursion / function calls; unbounded loops (loops are bounded, unrolled to depth k) |
Bounded loops (return/break/continue), unrolled to depth k |
Heap aliasing, full object semantics |
| A Python subset, plus a C subset front end | Full language semantics |
Verdicts:
EQUIVALENT up to bound N— no diverging input exists within the bound (proven by the symbolic stage; for the current loop-free subset this is complete over all inputs at the chosen width).COUNTEREXAMPLE: <input>— a concrete input where the two functions disagree, with both outputs.UNKNOWN— no counterexample found, but equivalence not proven (e.g. the symbolic stage declined to model something — see below). Never silently upgraded toEQUIVALENT.
Congruent never claims unconditional soundness. Every verdict carries its bound and assumptions.
How it works
Cheap checks first, expensive proof only when needed:
- Differential testing (
difftest.py) — property-based random + boundary inputs. Kills obvious non-equivalence in milliseconds. A counterexample here ends the run. - Symbolic execution → SMT (
symbolic.py+solver.py) — translate both functions' bounded behavior into logical constraints, assert inputs equal ∧ outputs differ, and ask Z3.UNSAT= equivalent within bound;SAT= the model decodes back to a concrete counterexample. - Bounded model checking — unroll loops to depth
k, verify up to that bound, report the bound. (Recursion / function calls are out of scope.)
input: fn_original, fn_candidate (source) + bound config
├─ parse → normalized typed IR (ir.py)
├─ Stage 1: differential tester ──► counterexample? ─► REPORT (difftest.py)
├─ Stage 2: symbolic engine (symbolic.py)
│ • symbolically execute each fn over fresh symbolic inputs
│ • collect path constraints + output expressions
│ • build (inputs equal) ∧ (outputs differ) → Z3 solve (solver.py)
│ • UNSAT → EQUIVALENT up to bound N
│ • SAT → decode model → COUNTEREXAMPLE
└─ REPORT (verdict, bound, counterexample, solver stats, caveats) (report.py)
The demo it's built to land
An LLM "simplifies" a midpoint calculation:
# original (correct under fixed-width ints)
def mid(lo: int, hi: int) -> int:
return lo + (hi - lo) // 2
# candidate (AI "simplification")
def mid(lo: int, hi: int) -> int:
return (lo + hi) // 2
Congruent catches it:
$ congruent original.py:mid candidate.py:mid --bound 8 --int-width 32
COUNTEREXAMPLE (stage: difftest)
inputs: lo = 1, hi = 2147483647
original: 1073741824
candidate: -1073741824 # 32-bit overflow in (lo + hi)
note: 32-bit two's-complement integers
And it proves the honest rewrites correct — distributivity over modular arithmetic, here, via Z3:
$ congruent original.py:f candidate.py:g # (x+y)*2 vs x*2 + y*2
EQUIVALENT (stage: symbolic, 0.00s)
equivalent (complete — no bound needed)
note: 32-bit two's-complement integers
note: complete: agree on all 32-bit inputs (no loops to bound)
Note what that verdict does not say: there is no loop and no list here, so the bound never binds — the pair is decided over the entire 32-bit input space. A result that genuinely is complete says so, rather than hiding behind a bound.
One screenshot of proof-or-counterexample on a real AI refactor communicates the whole value.
It also handles bounded loops. A reversed-accumulation refactor is proven equivalent up to the unroll bound:
$ congruent original.py:f candidate.py:g --bound 8 # sum i vs sum (n-1-i)
EQUIVALENT (stage: symbolic, 0.01s)
equivalent up to bound 8
note: 32-bit two's-complement integers
note: holds within bound: loops up to 8 iterations
And you can scope the question with a precondition — equivalence often only holds on part of the input domain:
$ congruent ident.py:f abs.py:g # x vs (x if x>=0 else -x)
COUNTEREXAMPLE (stage: difftest)
inputs: x = -1 # they disagree on negatives
$ congruent ident.py:f abs.py:g --assume 'x >= 0'
EQUIVALENT (stage: symbolic, 0.00s)
note: precondition: x >= 0
Declare a precondition inline with a leading assume(...) in the reference function, or pass --assume on the CLI.
And it reasons about list[int] inputs — here it proves a hand-written count equals len, for every list up to the length bound:
$ congruent original.py:f candidate.py:g # len(xs) vs count loop
EQUIVALENT (stage: symbolic, 0.00s)
note: holds within bound: lists/strings up to length 8, loops up to 8 iterations
Status: M0–M7 complete, plus the LLM closed-loop stretch. The differential stage catches counterexamples (overflow included) under a fixed-width integer model; the symbolic stage lowers both functions to Z3 bitvector expressions and returns
EQUIVALENT(UNSAT), aCOUNTEREXAMPLE(SAT, decoded to concrete inputs), orUNKNOWN. Supported: ints/bools, branches,for ... in range(...)andfor x in xsloops (bounded model checking) withreturn/break/continue,assume(...)preconditions, and boundedlist[int]both as inputs (len, iteration,xs[i]) and as outputs (build and return a list via literals ++), and boundedstr(literals,len,==,+, indexing incl. Python negative indices, iteration). Out-of-bounds access and divide-by-zero are modeled as runtime errors (a rewrite that crashes where the original didn't is a counterexample); falling off the end without returning yields Python'sNone— a value distinct from both a raised exception and any returned value. Counterexamples are minimized to the smallest failing input. An optional--cross-checkre-decides each query with CVC5. Benchmarks pass with zero unsound verdicts. See PROGRESS.md and ROADMAP.md.
Install
Congruent is distributed on PyPI as congruent-eq; you still import congruent
and run the congruent CLI.
pip install congruent-eq
Or from source:
git clone <repo-url> congruent
cd congruent
python -m pip install -e ".[dev]"
Requires Python 3.11+. The solver backend is z3-solver.
Usage
congruent path/to/original.py:func_name path/to/candidate.py:func_name --bound 8
| Flag | Default | Meaning |
|---|---|---|
--bound N |
8 |
Loop unroll depth and list/string-length bound |
--int-width W |
32 |
Bit width for the fixed-width integer model |
--assume EXPR |
— | Precondition on the inputs, e.g. --assume 'n >= 0' (repeatable) |
--no-minimize |
off | Report the first counterexample found, not the smallest |
--cross-check |
off | Re-decide with CVC5 and flag any disagreement (needs pip install "congruent-eq[cross-check]") |
--timeout S |
300 |
Give up on the solver after S seconds and report UNKNOWN (0 = no limit) |
Some queries are genuinely intractable — multiplying unknowns by unknowns in a
loop (a polynomial with symbolic coefficients) is the classic one. --timeout
bounds them: the verdict degrades to an honest UNKNOWN, never a hang and never
a false EQUIVALENT. See benchmarks/README.md.
Layout
src/congruent/
ir.py # Python AST → normalized typed IR
cfront.py # C front end (pycparser → the same IR)
difftest.py # differential prefilter + fixed-width concrete interpreter (Stage 1)
symbolic.py # symbolic interpreter → Z3 exprs (Stage 2)
solver.py # equivalence query, model decoding, minimization
backends.py # CVC5 cross-check (independent second opinion)
equiv.py # orchestration, escalation, Verdict data model
report.py # verdict formatting
refine.py # the LLM closed loop (AI proposes → Congruent verifies → feedback)
cli.py # `congruent a.py:f b.py:g --bound 8`
tests/ # 229 tests incl. fuzz + oracle soundness guards
examples/ # gallery of realistic AI-refactor pairs (Python + C), runner,
# closed_loop_demo.py (offline/--live), live_rewrite.py (your code)
benchmarks/ # recall gate, timing-vs-bound, self-validating fuzzer,
# realpy_fuzz.py (semantics oracle), numpy_oracle.py (wrapping oracle)
docs/demo.svg # the README demo image
docs/live_run.md # captured live-model sessions (caught, corrected, proven)
Gallery
examples/ holds realistic AI-refactor pairs — faithful rewrites
and subtly broken ones — with Congruent's verdict on each (binary-search
midpoint, clamping, list maximum, sum-to-n, counting). The largest entry,
water_bill.py, is a ~50-line tiered-billing routine
whose candidate was written by a live model and accepted only after proof; a
second, polyval.py, is numpy's own Horner loop
seed-optimized by a live model — both captured in
docs/live_run.md. Run them all:
python examples/run_gallery.py
Closed loop: AI proposes, Congruent verifies
The stretch feature (refine.py) closes the loop: an
LLM proposes a rewrite, Congruent checks it, and any counterexample is fed back
so the model can fix its own mistake — repeating until the rewrite is proven
equivalent within the bound. The loop never accepts an unverified rewrite.
python examples/closed_loop_demo.py # offline, deterministic (a scripted LLM)
python examples/closed_loop_demo.py --live # a real model via the Anthropic API
python examples/live_rewrite.py FILE.py:func # point the live loop at your own code
The demo shows Congruent catching a plausible-but-wrong refactor and guiding the
fix — e.g. an agent "simplifies" a midpoint to (a + b) // 2, Congruent returns
the exact overflowing input, and the next attempt reverts to the safe form and is
proven equivalent. This is not hypothetical: in a live run, a real model
(claude-opus-4-8) made exactly that mistake, was handed the overflowing input,
and came back with the proven-safe form — the unedited transcript is in
docs/live_run.md. The rewriter is pluggable via a Rewriter
protocol: ScriptedRewriter (offline, used by the demo and tests) or
AnthropicRewriter (pip install "congruent-eq[llm]", reads ANTHROPIC_API_KEY).
Benchmarks
python benchmarks/bench_recall.py # verdict vs. expectation over the eval set
python benchmarks/bench_scaling.py # solver time vs. --bound
python benchmarks/fuzz.py # random pairs, each verdict re-checked
python benchmarks/realpy_fuzz.py # interpreter vs. real Python (semantics oracle)
python benchmarks/numpy_oracle.py # wrapping vs. numpy fixed-width ints (overflow oracle)
numpy_oracle.py needs the oracle extra: pip install "congruent-eq[oracle]"
(from source: python -m pip install -e ".[dev,oracle]"). Everything else in that
block runs on the core install.
bench_recall.py exits non-zero if any verdict is unsound (a false EQUIVALENT
or false COUNTEREXAMPLE), so it doubles as a soundness gate. fuzz.py is the
deepest check: it generates random function pairs, asks Congruent, and then
independently re-validates each verdict against the concrete interpreter — so a
false verdict fails loudly. It re-verifies clean on the current code (3,000 random
pairs, 0 unsound, at its default settings), and tens of thousands more passed
across seven adversarial audit rounds (plus Z3↔CVC5 cross-checks); a small
deterministic batch runs in the test suite.
Two more oracles validate the interpreter every other check trusts, each against
a reference that shares no code with Congruent — one per half of the fixed-width
model. realpy_fuzz.py unparses generated IR back to real Python and diffs the
behavior (this caught the negative-indexing bug both stages shared); it runs wide
so nothing overflows, isolating semantics. numpy_oracle.py covers the other
half — the two's-complement wrapping itself — by re-evaluating random integer
functions with numpy's C fixed-width scalars at small widths where overflow is
the common case; it agrees with the interpreter across 8/16/32/64 (and matches
Congruent's arithmetic exhaustively over every 8-bit operand pair, overflow
edges included). Both run a deterministic slice in the test suite.
The measured operating envelope — solver time out to bound 1024, where the cliff is, and why the default bound is 8 — is documented in benchmarks/README.md.
Roadmap & progress
- ROADMAP.md — milestones M0→M7 and the out-of-scope list as future work.
- PROGRESS.md — current state, M0 checklist, and open design decisions.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file congruent_eq-0.0.1.tar.gz.
File metadata
- Download URL: congruent_eq-0.0.1.tar.gz
- Upload date:
- Size: 111.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75d1bf5d25e9ac61723558d84b694debc6e837bcf16c4bd858e4f82afebbcec7
|
|
| MD5 |
2975e7843b84d09e282c7edc60a0b783
|
|
| BLAKE2b-256 |
96604c3ae83307e8386074d2fba8a4a56413584fcb9e4aa527f3fbc6ec21f03b
|
Provenance
The following attestation bundles were made for congruent_eq-0.0.1.tar.gz:
Publisher:
publish.yml on satchmakua/congruent-eq
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
congruent_eq-0.0.1.tar.gz -
Subject digest:
75d1bf5d25e9ac61723558d84b694debc6e837bcf16c4bd858e4f82afebbcec7 - Sigstore transparency entry: 2232715145
- Sigstore integration time:
-
Permalink:
satchmakua/congruent-eq@b25afdd611dd198181db4df3443dcc116ba0907b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/satchmakua
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b25afdd611dd198181db4df3443dcc116ba0907b -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file congruent_eq-0.0.1-py3-none-any.whl.
File metadata
- Download URL: congruent_eq-0.0.1-py3-none-any.whl
- Upload date:
- Size: 48.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
40434f4b5a452c692c494a4be2dc57c6d3e48acd5c5d043ee4c73cdbc172b191
|
|
| MD5 |
6ad33ca46ebaa5037261e1dbcf5a3e50
|
|
| BLAKE2b-256 |
6ec668f0f5a04ad4d22d07f80470ffd5aed69e526ffdb8b1f528461c49c16c59
|
Provenance
The following attestation bundles were made for congruent_eq-0.0.1-py3-none-any.whl:
Publisher:
publish.yml on satchmakua/congruent-eq
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
congruent_eq-0.0.1-py3-none-any.whl -
Subject digest:
40434f4b5a452c692c494a4be2dc57c6d3e48acd5c5d043ee4c73cdbc172b191 - Sigstore transparency entry: 2232715673
- Sigstore integration time:
-
Permalink:
satchmakua/congruent-eq@b25afdd611dd198181db4df3443dcc116ba0907b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/satchmakua
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b25afdd611dd198181db4df3443dcc116ba0907b -
Trigger Event:
workflow_dispatch
-
Statement type: