Someone ran 50 random QF_UF queries against real Z3 and OxiZ disagreed on 17 of them. That is not a rounding error — that is a solver you cannot trust.
Today we released OxiZ 0.3.2 — a soundness release that started with an outside bug report and ended as a from-scratch audit of every code path it touched. GitHub issue #25 showed that the project’s own 168-case “Z3 parity” suite was a regression suite, not a differential-testing result: a random 50-instance QF_UF sample run against a real z3 binary found a 34% (17/50) disagreement rate. The reporter opened eight pull requests (#26–#33) against the bugs they found. Per project policy — there is no CLA or uniform contribution-provenance guarantee here yet — none of them were merged directly. Every one was read in full for its diagnostic value, and every fix in this release is an independent, from-scratch reimplementation, verified against a regression test derived from a minimal repro that fails on the pre-fix code. ~270 such tests were added in the process.
No C. No C++. No Fortran. OxiZ reimplements Z3 entirely in Rust — the SAT core, theory solvers, quantifier instantiation, optimization, and proof system are all memory-safe Rust you can read end to end. It compiles to a single static binary (or WASM) and runs everywhere. 0.3.2 is what that transparency is for: a bug report with reproducible counterexamples, applied line by line against a codebase with no black box to hide behind.
Why OxiZ 0.3.2 is a game changer
Before this release, four independent bug families could make OxiZ confidently wrong without a single crash:
- EUF congruence closure could merge two nodes that had already diverged, turning a genuinely
satformulaunsat - A non-Bool
ite, a Bool-sorted(=), or a Bool term used as a UF argument could each slip past congruence completion — a false-satfamily the reporter’s benchmark tripped over directly - Arithmetic and EUF never shared a numeric UF argument like the
3inf(3)— an arithmetic-entailed equality such asx=2, y=x+1 ⊢ y=3never reached EUF, sof(y)≠f(3)reportedsatregardless of what arithmetic had already proven - NLSAT conflict analysis could resolve in the wrong literal, drop a theory-forced literal silently, or read a stale decision level — three separate ways to turn
satintounsator vice versa
0.3.2 ends all of that:
- 168/168 Correct, 0 Wrong on the extended 19-logic differential suite, re-measured against real
z34.15.4 at release time — the same suite issue #25 called into question is now backed by tracked per-platform evidence, not a self-reported number - ~270 new regression tests, each derived from a minimal repro that fails on the pre-fix code — not just “tests pass” but “this exact bug cannot come back silently”
- Nine independent bug families fixed, from EUF signature staleness to a
distinct-over-constants polarity inversion to a DIMACS empty-clause parser gap - QF_NIA/QF_NRA nonlinear solving turned on by default, backed by a new stochastic model-repair engine and an exact
BigRationalevaluator that double-checks every candidate model before it’s trusted - 9,953 tests passing (
--all-features), 0 compiler warnings, 0 clippy warnings, confirmed at release time
This is not a blanket “100% Z3 compatibility” claim — the parity suite still doesn’t include plain QF_UF, the exact fragment the reporter’s own sample targeted. Issue #25 stays open on purpose: this release closes the bugs it led to, not the broader claim.
Technical Deep Dive: nine bugs, one shape each
EUF congruence + Bool/EUF encoding (oxiz-theories/src/euf/solver/congruence.rs, oxiz-solver/src/solver/encode/bool_euf_encoding.rs). Congruence closure republished a re-canonicalized node’s signature without evicting its old entry, so a stale signature-table hit could merge in a node that had since diverged. Separately, non-Bool ite, Bool-sorted =, and Bool terms in UF argument position each had a gap in how they reached EUF completion. All four are fixed with evict-and-reinsert signature maintenance plus explicit hoisting/completion for each encoding gap.
Arithmetic⇄EUF combination (oxiz-solver/src/solver/encode/numeric_purification.rs, oxiz-theories/src/arithmetic/solver.rs, oxiz-solver/src/solver/theory_manager/nelson_oppen.rs). Numeric UF arguments are now purified into fresh proxy variables with a get-value alias back to the original term; a bounded per-round “care graph” probes for entailed (dis)equalities via new Farkas-certificate probes; and for the genuinely non-convex case, a small explicit case-split disjunction is asserted before conceding sat.
NLSAT conflict analysis (oxiz-nlsat/src/solver/conflict.rs, solver/resample.rs). The 1-UIP resolution step now reconstructs every resolved-in literal from the trail itself instead of negating the wrong thing; theory-forced literals with no clause backing are tracked explicitly so an empty clause through one yields Unknown, never a fabricated Unsat; and a new witness ledger retries a different point from the same region instead of conceding infeasibility at decision level 0.
Parser and encoder edge cases. A distinct-over-constants query that constant-folds to TermKind::False now returns the correctly-signed literal (oxiz-solver/src/solver/encode.rs); define-fun call-site arguments are substituted by their exact TermId rather than re-derived by name (oxiz-core/src/smtlib/parser/); a lone 0 clause terminator in a DIMACS file — the empty, unconditionally-false clause — is no longer silently dropped (oxiz-sat/src/dimacs.rs); and pure-literal elimination now checks an explicit trail-status exclusion set instead of contradicting a fact the trail already forced.
Getting Started
Add OxiZ to your project:
cargo add oxiz
The distinct-over-constants fix, demonstrated — this query answered unsat before 0.3.2:
use oxiz_solver::Context;
let mut ctx = Context::new();
let output = ctx.execute_script(r#"
(set-logic QF_LIA)
(assert (distinct 5 2))
(check-sat)
"#).expect("script should parse and run");
assert_eq!(output[0], "sat");
// Before 0.3.2, `distinct` over two constants constant-folded to a Tseitin
// `False` node whose encoder returned the wrong-polarity literal for it —
// so a query that is trivially true (5 != 2) reported `unsat` instead.
Feature flags are unchanged: nlsat for nonlinear arithmetic, optimization for MaxSMT/OMT, spacer for CHC model checking, proof for DRAT/Alethe/LFSC export, full for everything.
What’s New in 0.3.2
Breaking (0.x API)
NlDispatchResult::Satgained a payload:Sat→Sat(Box<Interpretation>), carrying the witness model.SolverConfiggrew thirteen new public fields (use_vmtf,enable_bve,nonlinear_model_search, and others), all defaulting conservatively. Full struct-literal construction without..Default::default()needs updating.VMTF’s public API was rewritten from anOption<Var>linked list to a persistent decision cursor — it went from unused dead code to load-bearing.
Fixed
- EUF congruence closure’s stale-signature merge bug, and three independent Bool/EUF encoding gaps behind a false-
satfamily. - Arithmetic⇄EUF never sharing numeric UF arguments — the headline false-
satfamily on QF_UFLIA/QF_UFIDL. - A quantified
satverdict with no model-verification gate against ground disequalities. - Three independent NLSAT conflict-analysis bugs, including one that could report
Unsaton a genuinely satisfiablex·y=35. distinctover two constants,define-funcall-site argument substitution, a DIMACS empty-clause parsing gap, a pure-literal-elimination/trail contradiction, and an unsound-or-unjustified hyper-binary-resolution pass.
Added
- A CaDiCaL-inspired SAT search engine: VMTF branching actually wired into decisions, focused/stable mode alternation, trail-reuse restarts, phase rephasing.
- An opt-in SAT inprocessing toolkit: failed-literal probing, bounded variable elimination, equivalent-literal substitution, gate-congruence closure.
- Online LRAT proof production plus a new pure-Rust, forward-only LRAT checker with no external-tool shellout.
- QF_NIA/QF_NRA nonlinear solving on by default: an exact
BigRationalevaluator, a stochastic model-repair engine, and array/UF grammar reduction. - Equality-logic and finite-map preprocessing that turns exponential-blowup shapes (disjunctive equality chains, deep
itelookup tables) into linear ones.
Full itemized detail, crate by crate, is in the CHANGELOG.
Tips
- Re-verify any cached QF_UFLIA/QF_UFIDL model. If arithmetic ever entailed an equality that should have propagated to a UF argument, this release closes that gap — re-run any solve you cached results from before 0.3.2.
- Update
NlDispatchResult::Satmatch arms and full-struct-literalSolverConfigconstruction. Both are breaking changes; matchingSat(_)and using..Default::default()keeps you forward-compatible. - Try the new SAT inprocessing toolkit — it’s opt-in.
SolverConfig::enable_bve,enable_failed_literal_probing,enable_equiv_substitution, andenable_gate_congruenceare all off by default; turn them on for large, structured instances where preprocessing pays for itself. - Nonlinear solving (
QF_NIA/QF_NRA) is on by default now.SolverConfig::nonlinear_model_searchships true infast/balanced/thoroughpresets — if you were routing aroundUnknownon nonlinear queries, it’s worth re-testing. - LRAT proof checking no longer shells out.
oxiz_proof::check_lrat_proofis pure Rust end to end — useful if your CI sandboxes external tool execution. distinctanddefine-funedge cases are worth a re-check if your workload constant-foldsdistinctover literals or calls macros with non-Bool parameters that happen to share a name with a global constant.
This is the foundation
OxiZ is the formal-reasoning backbone of the COOLJAPAN ecosystem. OxiLean uses it as its SMT proof backend; Legalis-RS uses it for legal formal verification; OxiCAD, OxiCar, OxiEDA, OxiAutoRS, OxiMed, and OxiQuant all depend on oxiz-core/oxiz-solver/oxiz-proof for constraint solving; OxiAero leans on it for flight-critical verification; OxiML builds proof-backed model checks on top of it. Underneath, OxiZ relies on pure-Rust OxiARC for compression. The whole stack is C/C++/Fortran-free — sovereign from the SAT core all the way up to the application.
Repository: https://github.com/cool-japan/oxiz
Star the repo if you want an SMT solver that responds to a “you’re 34% wrong on this sample” bug report by re-deriving every fix from scratch and shipping the regression tests to prove it.
The era of trusting a solver’s parity claims without tracked evidence is over. Pure Rust formal reasoning is here — audited, honest, and sovereign.
— KitaSan at COOLJAPAN OÜ August 5, 2026