Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

bitwright is a Rust library for machine-integer expressions: values of 1 to 512 bits, built from the operators a processor has (arithmetic, bitwise logic, shifts and rotations, comparisons, extensions and extracts, bit counts), and IEEE 754 floating point in any format, on the bit-vectors that hold the encodings. It is for tools that reason about such expressions, for example binary analyzers, deobfuscators, decompilers and lifters, and it can be used from C, C++ and Python as well as Rust. It gives you three things:

  • Exact values. Every operator has total semantics: the SMT-LIB QF_BV definition, including division by zero and shifts past the width. Any expression has exactly one value for every assignment of its symbols.
  • Facts. For any expression, bitwright can tell which bits are known, which unsigned and signed ranges it lies in, and whether a comparison is provably true, provably false, or unknown.
  • Simplification you can trust. Expressions are rewritten by rules that are checked (exhaustively at small widths and by sampling up to 512 bits) before they may be used, and by normal-form passes that decide whole fragments at once (linear arithmetic, xor forms, truth tables of three or fewer atoms, comparison lattices, linear mixed boolean-arithmetic). Every simplifier run is bounded by budgets you choose, and termination is guaranteed by construction.

For deobfuscation, bitwright undoes mixed boolean-arithmetic, linear and nonlinear, with its own solver and proofs; folds opaque predicates from what is known about their operands; brings back the comparisons that lifted flag computations stand for, integer and floating-point; recognizes rotations, byte swaps and extensions in bit shuffles; and reduces checks of keyed hashes to their one solution. It works under path constraints, and each result says which of them it relied on.

Ways to use it

  • As a library, in Rust: build expressions or parse them, ask for facts and proofs, and run an engine of your choice (see Simplifying).
  • From C, C++, Python and JavaScript, through the bindings (JavaScript through WebAssembly), with the same results.
  • On lifted code: p-code, VEX and LLVM IR read into expressions (see Lifted code), and from a disassembler through its plugin scripts.
  • To verify a compiler: peephole transformations and pairs of LLVM IR functions proved or refuted under LLVM’s semantics (see Verifying compiler transformations).
  • From the command line: bitwright simplify deobfuscates an expression, and the other commands check, lint and document rule files (see The command line).
  • With your own rules, written in a small language, checked for soundness before they can be used (see Writing rules).
  • Next to an SMT solver: expressions go out as SMT-LIB and come back in, and every rule has an obligation any solver can prove (see SMT-LIB).

What bitwright does not do

It does not guess. Input it cannot handle stays unchanged and is reported as such. A pass that is not sure a rewrite is smaller does not make it, and an MBA answer from an external solver is published only with evidence you accept. There is no global state, no environment variable changes a result, and the same input gives the same output on every run.

Features

FeatureDefaultAdds
checkyesthe rule soundness checker, evidence and proof ledgers
smtlibnoSMT-LIB export and import, rule obligations for any SMT solver
mbanothe MBA service: lowering, solver and prover traits, the evidence gate
eqsatnoa bounded equality-saturation search for alternative expressions

This book

The chapters follow the order you are likely to need them: building and evaluating expressions, their semantics, floating point, facts, simplification, writing and checking your own rules, then the optional services, the command line and the bindings. Examples and Examples in Python, C and C++ are worked tasks from reverse engineering, one per section, to start from. Every code example in this book, in every language, is compiled and run as a test.

Getting started

Add bitwright to Cargo.toml:

[dependencies]
bitwright = "0.16"

Values

A BitVec is a value of a given Width (1 to 512 bits). Every operator is total:

#![allow(unused)]
fn main() {
use bitwright::{BinOp, BitVec, Width};

let x = BitVec::from_u64(Width::W8, 200)?;
let y = BitVec::from_u64(Width::W8, 100)?;
assert_eq!(BitVec::apply_bin(BinOp::Add, &x, &y)?.to_u64(), Some(44)); // modulo 2^8
let zero = BitVec::zero(Width::W8);
assert!(BitVec::apply_bin(BinOp::UDiv, &x, &zero)?.is_ones()); // x / 0 is all ones
Ok::<(), Box<dyn std::error::Error>>(())
}

Expressions

Expressions live in a Context, an arena that owns them. An Expr is a small handle into it. Equal expressions are the same node (hash-consing), and construction already puts them in a canonical form, so building the same expression twice gives the same handle:

#![allow(unused)]
fn main() {
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let a = cx.parse("y + x", &o)?;
let b = cx.parse("x + y", &o)?;
assert_eq!(a, b);
// Construction canonicalizes and folds constants, but does not simplify further.
let c = cx.parse("(x - (2 + 3)) + 5", &o)?;
assert_eq!(cx.display(c).to_string(), "x - 5 + 5");
Ok::<(), bitwright::Error>(())
}

You can also build expressions directly:

#![allow(unused)]
fn main() {
use bitwright::{BinOp, CmpOpExt, Context, UnOp, Width};

let mut cx = Context::new();
let x = cx.symbol("x", Width::W16)?;
let y = cx.symbol("y", Width::W16)?;
let sum = cx.bin(BinOp::Add, x, y)?;
let not_x = cx.un(UnOp::Not, x)?;
let less = cx.cmp(CmpOpExt::Ult, sum, not_x)?; // a 1-bit value
let picked = cx.select(less, sum, not_x)?;
let low = cx.trunc(picked, Width::W8)?;
assert_eq!(cx.width(low)?, Width::W8);
Ok::<(), bitwright::Error>(())
}

Symbols are keyed by a name, a number or a fresh key, and each key has one width in a context.

Evaluating

eval takes the values of the symbols from an environment, such as a map or a closure:

#![allow(unused)]
fn main() {
use std::collections::BTreeMap;
use bitwright::{BitVec, Context, ParseOptions, SymbolKey, Width};

let mut cx = Context::new();
let e = cx.parse("(x * 3) >>u 1", &ParseOptions::width(Width::W8))?;
let env: BTreeMap<SymbolKey, BitVec> =
    [(SymbolKey::from("x"), BitVec::from_u64(Width::W8, 100)?)].into_iter().collect();
assert_eq!(cx.eval(&[e], &env)?[0].to_u64(), Some(22)); // (300 mod 256) / 2
Ok::<(), Box<dyn std::error::Error>>(())
}

Simplifying

The built-in simplifier is one call:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let e = cx.parse("(x - 5) + 5 + (y & ~y)", &ParseOptions::width(Width::W32))?;
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x");
Ok::<(), bitwright::Error>(())
}

The chapter Simplifying covers strategies, budgets and what the result tells you.

Semantics

Widths and values

A width is 1 to 512 bits. A value is an unsigned bit pattern of its width; operators that care about sign (sdiv, srem, >>s, <s, sext, …) read it as two’s complement. Operands of an operator have the same width unless the operator says otherwise (extensions, extracts, concat, select’s 1-bit condition), and a width mismatch is an error, never a silent cast.

Total semantics

Every operator is defined for every input, exactly as SMT-LIB QF_BV defines it:

OperatorResult
udiv(x, 0)all ones
urem(x, 0)x
sdiv(x, 0)1 if x <s 0, else -1
sdiv(smin, -1)smin
srem(x, 0)x
x << c, x >>u c with c ≥ W0
x >>s c with c ≥ Wthe sign, in every bit
rotl(x, c), rotr(x, c)rotation by c mod W (any W)
clz(0), ctz(0)W

Floating-point operations are total too, each result pinned where IEEE 754 leaves it open (see Floating point). Because there is no undefined behavior, equal handles always mean equal values, and every rule is a closed statement a solver can check. A host that needs fault semantics (a division that traps) builds the trap condition as its own expression; see bitwright::traps.

#![allow(unused)]
fn main() {
use bitwright::{BitVec, BinOp, Width};

let w = Width::new(12)?;
let smin = BitVec::smin(w);
let minus_one = BitVec::ones(w);
assert_eq!(BitVec::apply_bin(BinOp::SDiv, &smin, &minus_one)?, smin);
let big = BitVec::from_u64(w, 40)?;
assert!(BitVec::apply_bin(BinOp::Shl, &minus_one, &big)?.is_zero());
Ok::<(), Box<dyn std::error::Error>>(())
}

The text syntax

The parser and printer share one syntax. Unsigned and signed variants are always explicit: >>u and >>s, <u and <s; a bare >> or < is an error. Widths are inferred where possible and written with a colon where not (0xff:8, x:64); casts take their width in angle brackets (zext<32>(x), trunc<8>(x), extract<lo, len>(x)).

#![allow(unused)]
fn main() {
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let e = cx.parse("select(x <s 0, -x, x) + zext<32>(trunc<8>(y))", &o)?;
let text = cx.display(e).to_string();
// Printing and parsing again gives the same node.
assert_eq!(cx.parse(&text, &o)?, e);
// A shift without its signedness is an error, not a guess.
assert!(cx.parse("x >> 1", &o).is_err());
Ok::<(), bitwright::Error>(())
}

Canonical form

Construction puts every node in a canonical form that does not depend on the order you built things in or on which context you used: commutative operands are ordered by a structural key, constants go right, all-constant subterms are folded, and a handful of identities that never lose information (x + 0, x & x, double negation, …) are applied. Everything beyond that is the simplifier’s job, so construction stays cheap and predictable.

#![allow(unused)]
fn main() {
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
for (written, built) in [
    ("(y | x) + (x & y)", "(x & y) + (x | y)"), // commutative operands in one order
    ("5 + x", "x + 5"),                         // constants on the right
    ("x + 2 * 3", "x + 6"),                     // constant subterms folded
    ("~~x & x", "x"),                           // identities that lose nothing
    ("(x + 1) + 2", "x + 1 + 2"),               // not reassociated: the simplifier's job
] {
    let e = cx.parse(written, &o)?;
    assert_eq!(cx.display(e).to_string(), built);
}
Ok::<(), bitwright::Error>(())
}

Floating point

A floating-point value is a bit-vector holding an IEEE 754 interchange encoding, the way a register or memory holds it. bitwright evaluates, builds, simplifies and reasons about IEEE 754 binary arithmetic on such values, in any format, under every rounding mode, exactly: every operation is defined for every input pattern and gives exactly one result pattern. The arithmetic is done in software, never on the host’s floating-point unit, so results do not depend on the machine or on a flush-to-zero or rounding mode a host program may have set.

Formats

A format (eb, sb) has eb exponent bits and sb significand bits, the hidden bit included (SMT-LIB’s convention), so an encoding is eb + sb bits wide: a sign bit, the biased exponent, then the trailing sb − 1 significand bits. Any 2 ≤ eb ≤ 31, sb ≥ 2, eb + sb ≤ 512 is a format (FpFormat::new); the named ones:

Format(eb, sb)WidthName in the text syntax
binary16(5, 11)16f16
bfloat16(8, 8)16bf16
binary32(8, 24)32f32
binary64(11, 53)64f64
binary128(15, 113)128f128
binary256(19, 237)256f256
x87 extended (values)(15, 64)79—

x87’s 80-bit memory encoding has an explicit integer bit; x87_load and x87_store convert between it and (15, 64), with x87’s reading of its non-canonical encodings: a pseudo-denormal is the normal value it stands for, and an unnormal, a pseudo-infinity or a pseudo-NaN is invalid, so it loads as the NaN.

Semantics

Results are correctly rounded: the exact mathematical result, rounded once in the operation’s rounding mode (RoundingMode: Rne to nearest with ties to even, Rna ties away from zero, Rtp toward +∞, Rtn toward −∞, Rtz toward zero), with gradual underflow, and past the largest finite value ∞ or the largest finite value as the mode says. Where IEEE 754 or SMT-LIB leave a result open, bitwright fixes one, the same on every host:

bitwright
A NaN resultthe format’s canonical quiet NaN: sign 0, exponent all ones, only the top significand bit set (0x7fc00000 in binary32). NaN payloads do not propagate.
neg, abs, copysignchange the sign bit only, of a NaN too (IEEE 754 §5.5.1)
min, maxIEEE 754-2019 minimumNumber and maximumNumber: a NaN operand is ignored (two give the NaN), and −0 < +0
Conversion to an integerrounded by the operation’s mode, then saturated to the integer’s range; a NaN converts to 0
remIEEE remainder: a − n·b, n the integer nearest a / b, ties to even (exact)
A zero sum x + (−x)+0, or −0 toward −∞

The semantics are those of RISC-V, of ARM with default NaNs, and of SMT-LIB’s single NaN. An instruction set that propagates NaN payloads or has an asymmetric minimum (x86’s minss is a < b ? a : b) is a composition of these operations with select, which bitwright simplifies like any other expression.

Values

FpFormat has every operation on BitVecs; the operands’ widths must be the format’s.

#![allow(unused)]
fn main() {
use bitwright::fp::{FpCmpOp, FpFormat, RoundingMode::*};
use bitwright::{BitVec, Width};

let f = FpFormat::F64;
let (a, b) = (BitVec::from_f64(1.0), BitVec::from_f64(3.0));
// 1/3 to nearest, and rounded up: one unit in the last place apart.
let near = f.div(Rne, &a, &b)?;
let up = f.div(Rtp, &a, &b)?;
assert_eq!(near.to_f64(), Some(1.0 / 3.0));
assert_eq!(up.to_f64().unwrap().to_bits(), (1.0f64 / 3.0).to_bits() + 1);
// A NaN result is the canonical one, whatever the operands were.
let nan = f.sqrt(Rne, &BitVec::from_f64(-1.0))?;
assert_eq!(nan, f.nan());
assert!(!f.cmp(FpCmpOp::Eq, &nan, &nan)?);
// Conversions saturate: 1e10 does not fit 32 bits; a NaN gives 0.
assert_eq!(f.to_sint(Rtz, &BitVec::from_f64(1e10), Width::W32)?.to_u64(), Some(0x7fff_ffff));
assert_eq!(f.to_sint(Rtz, &nan, Width::W32)?.to_u64(), Some(0));
// Any format: binary16 has 11 bits of precision, so 2049 rounds to 2048 or 2050.
let h = FpFormat::F16;
let i = BitVec::from_u64(Width::W16, 2049)?;
assert_eq!(h.from_uint(Rne, &i), h.from_uint(Rne, &BitVec::from_u64(Width::W16, 2048)?));
assert_eq!(h.from_uint(Rtp, &i), h.from_uint(Rne, &BitVec::from_u64(Width::W16, 2050)?));
Ok::<(), Box<dyn std::error::Error>>(())
}

Expressions

Context::fp builds an operation node (FpOp): Add, Mul, Div, Fma, Sqrt, RoundToIntegral and the conversions take a rounding mode; Rem, Min, Max and the comparisons Eq, Lt, Le (1 bit) do not. The operations that look only at the sign and exponent are built from bit-vector operators, so every pass sees through them: Context::fp_neg is x ^ sign, Context::fp_abs is x & ~sign, each test of Context::fp_test (isNaN, isInfinite, isZero, isNormal, isSubnormal, isNegative, isPositive) is one unsigned comparison of the encoding, Context::fp_sub is the sum with the negation, and > and ≥ are < and ≤ swapped.

The text syntax writes an operation as fp.<op>[.<mode>]<format>(operands), the format by name (.f32) or as <eb, sb>. Every identifier that starts with fp. is reserved.

TextOperation
fp.add.rne.f32(a, b), fp.sub, fp.mul, fp.divarithmetic
fp.fma.rtz.f64(a, b, c)a · b + c, rounded once
fp.sqrt.rne.f64(a), fp.round.rtn.f64(a)square root, rounding to an integral value
fp.rem.f64(a, b), fp.min.f64(a, b), fp.max.f64(a, b)exact, no mode
fp.eq.f32(a, b), fp.lt, fp.le, fp.gt, fp.gecomparisons, 1 bit
fp.neg.f32(a), fp.abs, fp.copysign.f32(a, b)sign operations
fp.isnan.f32(a), fp.isinf, fp.iszero, fp.isnormal, fp.issubnormal, fp.isneg, fp.ispostests, 1 bit
fp.convert.rne.f64.f32(a), fp.convert.rne<11, 53, 3, 4>(a)between formats
fp.from_sbv.rne.f64(i), fp.from_ubvfrom a signed or unsigned integer of any width
fp.to_sbv.rtz.f64<32>(a), fp.to_ubvto an integer of the given width, saturating
fp.x87_load(x), fp.x87_store(a)x87’s 80-bit encoding
#![allow(unused)]
fn main() {
use bitwright::fp::{FpFormat, FpOp, RoundingMode};
use bitwright::{BitVec, Context, ParseOptions, SymbolKey};

let mut cx = Context::new();
let o = ParseOptions::default();
let e = cx.parse("fp.fma.rne.f32(x, y, fp.neg.f32(z))", &o)?;
assert_eq!(cx.display(e).to_string(), "fp.fma.rne.f32(x, y, z ^ 0x80000000)");
// Operations on constants fold, exactly.
let tenth = cx.constant(&BitVec::from_f32(0.1))?;
let s = cx.fp(FpOp::Add(RoundingMode::Rne), FpFormat::F32, &[tenth, tenth])?;
assert_eq!(cx.as_const(s)?.and_then(|v| v.to_f32()), Some(0.2));
// And evaluate at a point.
let env = [
    (SymbolKey::from("x"), BitVec::from_f32(2.0)),
    (SymbolKey::from("y"), BitVec::from_f32(3.0)),
    (SymbolKey::from("z"), BitVec::from_f32(1.0)),
];
assert_eq!(cx.eval(&[e], &env[..])?[0].to_f32(), Some(5.0));
Ok::<(), bitwright::Error>(())
}

Rules can rewrite floating-point operations too, generic in the format and the rounding mode, and the checker checks them in every small format.

Construction applies the identities that hold bit for bit on every encoding (a NaN operand gives the canonical NaN on both sides, zeros keep their signs) and add no node: fma(x, 1, y) is x + y, fma(x, y, −0) is x · y except toward −∞, x · 2 and x / ½ are x + x, (−a) · (−b) is a · b (and so for /), −a < −b is b < a, x < x is false, rounding to an integral value twice is rounding once, and a signed conversion of a zero or sign extension converts the operand itself.

Identities that hold only on some encodings (x · 1 = x fails for a NaN with a payload, x + (−0) = x for +0 toward −∞) are the built-in rules of the group core.float, each guarded by what it needs: that an operand is no NaN (fp.not_nan), no zero (fp.nonzero) or finite (fp.finite), which the facts prove or do not. Dividing by a power of two whose reciprocal is a normal number becomes multiplying by the reciprocal, which is the same real number rounded once in the same mode. So a float that came from an integer, which the facts know is no NaN, loses a multiplication by 1.0, and x · 1.0 of an unknown x stays:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let engine = Engine::standard();
let mut cx = Context::new();
let o = ParseOptions::width(Width::W64);
for (float, simplified) in [
    ("fp.mul.rne.f64(fp.from_sbv.rne.f64(i:32), 0x3ff0000000000000)", "fp.from_sbv.rne.f64(i)"),
    ("fp.mul.rne.f64(x, 0x3ff0000000000000)", "fp.mul.rne.f64(x, 0x3ff0000000000000)"),
    ("fp.div.rtz.f64(x, 0x4010000000000000)", "fp.mul.rtz.f64(x, 0x3fd0000000000000)"),
    ("fp.eq.f64(fp.sqrt.rne.f64(fp.from_ubv.rne.f64(u:32)), fp.sqrt.rne.f64(fp.from_ubv.rne.f64(u:32)))", "1:1"),
] {
    let e = cx.parse(float, &o)?;
    let out = engine.simplify(&mut cx, e)?;
    assert_eq!(cx.display(out.expr).to_string(), simplified, "{float}");
}
Ok::<(), bitwright::Error>(())
}

They also divide a finite nonzero number by itself (1), subtract it from itself (+0, or −0 toward −∞), take the square root of a square that is a normal number (|x|, to nearest), and widen a number and narrow it back. The rules are listed in the rule catalog, and a rule of your own can use the same guards (see Writing rules).

A host that observes floats only as values, as a compiler’s fast-math does, can also let the engine treat every NaN as one value: Strategy::with_float_values(true) (or bitwright simplify --float-values) applies the rules of core.float_values, which hold that way but may change a NaN’s payload or sign: x · 1 and x + (−0) (to nearest) are x, x · −1 is −x, min(x, x) and max(x, x) are x, widening and narrowing back is x.

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Strategy};
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let e = cx.parse("fp.mul.rne.f32(x, 0x3f800000)", &ParseOptions::width(Width::W32))?;
let exact = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(exact.expr, e); // a NaN `x` with a payload would lose it
let values = Engine::builder()
    .builtin()
    .strategy(Strategy::standard().with_float_values(true))
    .build()?;
let out = values.simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x");
Ok::<(), Box<dyn std::error::Error>>(())
}

Comparisons and class tests of one operand combine as the bit-vector conditions do: a lifted ucomiss followed by ja, (¬(x < y ∨ unordered)) ∧ ¬(x = y ∨ unordered), is y < x, and iszero(x) ∨ issubnormal(x) is one unsigned comparison of x without its sign bit (see Examples).

Facts

The facts of a floating-point result come from its operands’ facts, read as sets of floats: whether a NaN may occur, and an interval of values per sign. An operation is applied to the ends of those intervals in its own rounding mode (correct rounding is monotonic, so the ends bound every result), and falls back to every value where a special case may occur (0 · ∞, ∞ − ∞, a divisor that may be 0). So facts show that an integer converted to a float is never a NaN, that the square root of a non-negative value is not either, or that a converted byte is below 256, and the simplifier folds such questions:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions};

let mut cx = Context::new();
let e = cx.parse("fp.isnan.f64(fp.mul.rne.f64(fp.from_sbv.rne.f64(a:16), fp.from_sbv.rne.f64(b:16)))",
                 &ParseOptions::default())?;
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "0:1");
Ok::<(), bitwright::Error>(())
}

SMT-LIB

With feature smtlib, floating-point operations export to SMT-LIB’s FloatingPoint theory (QF_BVFP): an operand is read with ((_ to_fp eb sb) x), and a result’s bits are declared and pinned by one assertion (SMT-LIB has no standard fp.to_ieee_bv, since a NaN has many encodings), the canonical NaN included. What SMT-LIB leaves open is written out as bitwright defines it. smtlib::import reads the FloatingPoint theory back: the float sorts, literals and special values, every fp. operator, to_fp from bits, floats, integers and real constants (rounded exactly), and z3’s fp.to_ieee_bv.

How it is checked

  • An independent implementation, bitwright-ref::fp, written from this specification alone by exact rational arithmetic, agrees with bitwright on every input of every format of up to 8 bits, every operation under every mode (fma on every triple up to 6 bits), and on random operands of binary16, bfloat16, binary32, binary64, x87, binary128 and a 512-bit format.
  • binary32 and binary64 agree with the host’s hardware (round to nearest even), and the integer frames of the software arithmetic agree with each other.
  • The exported terms agree with Bitwuzla on binary16 to binary128 and with z3 on formats of precision 4 and more. (z3 5.1.0 is wrong below that: its fma at precision 3 or less and its roundToIntegral with a 2-bit exponent.)
  • Every fact transfer is checked sound on every format of 4 to 6 bits against every member of structured and random operand facts.

Facts and proofs

For any expression, Context::facts returns what bitwright knows about its value without knowing the symbols: known bits, an unsigned strided interval (the values lo, lo + stride, …, hi) and a signed range, combined so that each part tightens the others. Facts are always sound: every value the expression can take lies within them.

#![allow(unused)]
fn main() {
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let e = cx.parse("(x << 4) | 3", &ParseOptions::width(Width::W8))?;
let f = cx.facts(e)?;
// The low four bits are known: 0011.
assert_eq!(f.known().known().to_u64(), Some(0x0f));
assert_eq!(f.known().known_one().to_u64(), Some(0x03));
assert_eq!(f.urange().lo().to_u64(), Some(3));
Ok::<(), bitwright::Error>(())
}

The stride carries what arithmetic keeps. (x & 7) * 3 + 1 takes the values 1, 4, 7, …, 22, so it is never 5, although 5 lies between its bounds; and known bits and the interval tighten each other both ways (an odd value between 4 and 10 is between 5 and 9, and a stride of 4 fixes the two low bits):

#![allow(unused)]
fn main() {
use bitwright::{BitVec, Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let e = cx.parse("(x & 7) * 3 + 1", &o)?;
let u = cx.facts(e)?.urange();
assert_eq!((u.lo().to_u64(), u.hi().to_u64(), u.stride()), (Some(1), Some(22), 3));
let q = cx.parse("(x & 7) * 3 + 1 == 5", &o)?;
assert_eq!(cx.exact(q)?, Some(BitVec::from_bool(false)));
Ok::<(), bitwright::Error>(())
}

Facts are computed on demand, iteratively (no recursion, so deep expressions are fine), and cached per node for the life of the context. Each query is bounded by a work cap (ContextConfig::fact_work): when the cap is reached the answer is the sound “anything”, and the next query continues where this one stopped.

Proofs

Context::prove answers a question with Truth::True, Truth::False or Truth::Unknown. Unknown means the facts do not decide it, never that it is false:

#![allow(unused)]
fn main() {
use bitwright::{CmpOpExt, Context, ParseOptions, Query, Truth, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W16);
let small = cx.parse("zext<16>(trunc<8>(x))", &o)?;
let limit = cx.parse("256", &o)?;
assert_eq!(cx.prove(Query::Cmp(CmpOpExt::Ult, small, limit))?, Truth::True);
let odd = cx.parse("x | 1", &o)?;
assert_eq!(cx.prove(Query::IsZero(odd))?, Truth::False);
let y = cx.parse("y", &o)?;
assert_eq!(cx.prove(Query::IsZero(y))?, Truth::Unknown); // y may or may not be zero
Ok::<(), bitwright::Error>(())
}

Query::Injective and Query::Bijective ask whether an expression is an invertible function of one of its subexpressions; see Invertibility.

Assumptions

Facts about a path (after a branch, for example) are given as Assumptions. They never change the context’s own facts; queries under assumptions use a separate overlay. The next chapter, Constraints, covers predicates, propagation, and what results rely on.

#![allow(unused)]
fn main() {
use bitwright::{
    Assumptions, BitVec, CmpOpExt, Context, Facts, KnownBits, ParseOptions, Query, Truth, Width,
};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W8);
let x = cx.parse("x", &o)?;
let odd = cx.parse("x & 1", &o)?;
let zero = cx.parse("0", &o)?;
// On this path, x is known to have its low bit set.
let kb = KnownBits::new(BitVec::zero(Width::W8), BitVec::one(Width::W8)).unwrap();
let mut a = Assumptions::new();
a.assume(&mut cx, x, Facts::from_known(kb))?;
assert_eq!(cx.prove_with(Query::Cmp(CmpOpExt::Ne, odd, zero), &a)?, Truth::True);
assert_eq!(cx.prove(Query::Cmp(CmpOpExt::Ne, odd, zero))?, Truth::Unknown);
Ok::<(), bitwright::Error>(())
}

What holds of a symbol everywhere, not just on one path, is declared instead: Context::declare_known(x, kb) makes the known bits part of the symbol’s meaning. The context’s own facts then start from them, and no run pays for assumptions. The book’s Inside a compiler shows it.

Rule guards (the next chapters) are answered by these same facts, which is why a guard can only ever say “proven”, never “disproven”.

Constraints

A constraint is something you guarantee about the values that occur: a branch condition on the path being analysed, an ABI invariant, a division known not to fault. It restricts values and never changes what an operator means. So every proof made under constraints is still a proof, valid wherever those constraints hold, and the simplifier can use them to fire guarded rules and fold what they imply.

Assumptions holds a set of constraints. There are two kinds:

  • assume_true(p) and assume_false(p) take a 1-bit predicate: idx <u n after a jb that was taken, (rsp & 15) == 0 at a call boundary, !trap on a path that does not fault.
  • assume(e, facts) states known bits or ranges of any expression directly.

Each call returns the constraint’s ConstraintId, which is its position, counting from 0.

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Run};
use bitwright::{Assumptions, Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W64);
let mut a = Assumptions::new();
let aligned = cx.parse("(rsp & 15) == 0", &o)?;
let taken = cx.parse("idx <u n", &o)?;
let abi = a.assume_true(&mut cx, aligned)?;
let path = a.assume_true(&mut cx, taken)?;

let e = cx.parse("((rsp + 32) & 15) + zext<64>(idx <=u n)", &o)?;
let out = Engine::standard().run(&mut cx, &[e], Run::default().with_assumptions(&a))?;
let r = out.roots[0];
assert_eq!(r.expr, cx.parse("1", &o)?);
// The result relies on both constraints.
assert!(r.relies_on.may_use(abi) && r.relies_on.may_use(path));
Ok::<(), bitwright::Error>(())
}

Propagation

Adding a constraint propagates it, within a fixed number of steps.

Backwards to the operands. A constraint on an expression gives facts about its operands:

  • (rsp & 15) == 0 makes the low four bits of rsp known to be zero;
  • x + 8 <u 64 bounds x;
  • zext<32>(b) == 0x41 makes b exactly 0x41;
  • ctz(m) == 3 makes the low four bits of m exactly 1000;
  • p && q assumed true makes both true, and p || q assumed false makes both false.

Every backward transfer is checked exhaustively at small widths, like the forward ones.

Between comparisons of the same two expressions. Assuming x <u y also decides:

  • x <=u y (true),
  • x == y (false),
  • y <u x (false).

This holds for every expression over the pair, not just the one assumed. It works for any two expressions, not only constants, and the propagation also bounds each side by the other’s range. Equalities are transitive: after x == y and y == z, the constraint x != z is a contradiction, and y <u w decides x <u w.

Constraints that contradict each other make the set infeasible, and conflict() names the constraints involved. An infeasible set would justify any rewrite, so the simplifier ignores it: it simplifies as it would with no constraints, and reports that nothing relies on them. The one place it matters is prove_under, which answers True (anything holds where nothing can) and reports the conflicting constraints as what the answer relies on.

#![allow(unused)]
fn main() {
use bitwright::{Assumptions, Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W8);
let mut a = Assumptions::new();
let lo = cx.parse("x <u 5", &o)?;
let hi = cx.parse("x >u 7", &o)?;
let first = a.assume_true(&mut cx, lo)?;
let second = a.assume_true(&mut cx, hi)?;
let conflict = a.conflict().unwrap();
assert!(conflict.may_use(first) && conflict.may_use(second));
Ok::<(), bitwright::Error>(())
}

What a result relies on

Every RootOutcome reports relies_on, a Reliance: the constraints that the rewrites producing the result relied on.

  • An empty reliance means the result holds everywhere.
  • Otherwise the result holds wherever the constraints it relies on hold.

This matters when the scope of a constraint is narrower than the use of the result. Put the invariants that hold everywhere (ABI rules, facts about the whole program) first, and the path conditions after them. Then relies_on.all_before(first_path_id) tells you whether a rewrite can be committed outside the path.

Reliance over-approximates: it may name a constraint that was not really needed, but it never misses one. may_use(id) == false and all_before(id) == true are therefore definite. Constraints from the 64th on are tracked together.

Context::prove_under answers a Query together with what the answer relies on, and Context::facts_under gives an expression’s facts together with theirs.

Forking

Cloning an Assumptions is cheap: clones share their contents until one of them changes. A symbolic executor can build the invariants once and extend a clone per path. Constraints keep their ids in every clone, so the ConstraintIds of the invariants mean the same thing in every path.

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Run};
use bitwright::{Assumptions, Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W16);
let mut invariants = Assumptions::new();
let aligned = cx.parse("(sp & 15) == 0", &o)?;
invariants.assume_true(&mut cx, aligned)?; // constraint 0 in every path
let cond = cx.parse("x == 7", &o)?;
let mut taken = invariants.clone();
let mut not_taken = invariants.clone();
let t = taken.assume_true(&mut cx, cond)?;
not_taken.assume_false(&mut cx, cond)?;

let e = cx.parse("zext<16>(x == 7)", &o)?;
let engine = Engine::standard();
let on_taken = engine.run(&mut cx, &[e], Run::default().with_assumptions(&taken))?.roots[0];
let on_not_taken = engine.run(&mut cx, &[e], Run::default().with_assumptions(&not_taken))?.roots[0];
assert_eq!(on_taken.expr, cx.parse("1", &o)?);
assert_eq!(on_not_taken.expr, cx.parse("0", &o)?);
// It relies on the path condition, so it holds only on that path.
assert!(!on_taken.relies_on.all_before(t));
// This one relies only on the invariant, so it holds on every path.
let slot = cx.parse("(sp + 16) & 15", &o)?;
let r = engine.run(&mut cx, &[slot], Run::default().with_assumptions(&taken))?.roots[0];
assert_eq!(r.expr, cx.parse("0", &o)?);
assert!(r.relies_on.all_before(t));
Ok::<(), bitwright::Error>(())
}

The simplifier’s memo is keyed by the exact constraint set, so results never leak between paths.

Faults and undefined values

bitwright’s division is total, so a host that models a faulting division describes the fault with a trap guard (traps::udiv, traps::sdiv, and so on). On a path that does not fault, assume the guard false:

#![allow(unused)]
fn main() {
use bitwright::{Assumptions, Context, ParseOptions, Query, Truth, Width, traps};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let (q, d) = (cx.parse("q", &o)?, cx.parse("d", &o)?);
let trap = traps::udiv(&mut cx, q, d)?;
let mut a = Assumptions::new();
let no_fault = a.assume_false(&mut cx, trap)?;
let p = cx.prove_under(Query::IsNonZero(d), &a)?;
assert_eq!(p.truth, Truth::True);
assert!(p.relies_on.may_use(no_fault));
Ok::<(), bitwright::Error>(())
}

A value an ISA leaves undefined, such as a flag after some instructions, needs no constraint. Model it as a fresh symbol: nothing is known about it, and that is exactly what the ISA guarantees.

Checking a rewrite with a solver

With feature smtlib, smtlib::equivalence_query_under(cx, before, after, &a, relies_on) writes a script that asserts exactly the constraints the rewrite relied on. A solver’s unsat then proves the rewrite valid wherever those constraints hold, independently of bitwright.

Extension operations

An instruction set has operations that bitwright’s operators express only clumsily or not at all: a flags bundle, a funnel shift, a vendor-defined count. A host registers such an operation as an ExtOp. It is total, deterministic, and takes 1 to 3 arguments. It has 1 to 8 outputs, and each output is a node of its own.

An operation provides:

  • signature: its outputs’ roles and widths, given the argument widths;
  • eval: the exact values;
  • optionally known_bits, for facts;
  • optionally expand: the same operation built from bitwright’s own operators;
  • optionally smtlib: a term for solvers;
  • optionally invertible and invert: which outputs are injective or bijective in which argument, and the inverse at a value (see Invertibility).

Registration runs a self-test at the argument widths the signature accepts, mixed widths included, up to 512 bits. It checks that eval is deterministic and returns the declared widths, that a commutative operation really is, that known_bits and expand agree with eval, and that invert undoes eval wherever invertibility is declared. The test samples inputs, so it catches most broken operations, not all of them.

#![allow(unused)]
fn main() {
use std::sync::Arc;
use bitwright::ext::{ExtOp, ExtSig, Registry};
use bitwright::{BitVec, BinOp, CmpOpExt, Context, ContextConfig, KnownBits, SymbolKey, Width};

/// x86 `add` with its carry flag: the sum and CF.
struct AddCf;
impl ExtOp for AddCf {
    fn name(&self) -> &str { "x86.add_cf" }
    fn signature(&self, args: &[Width]) -> Result<ExtSig, String> {
        match args {
            [a, b] if a == b => Ok(ExtSig::new(&[("sum", *a), ("cf", Width::W1)])),
            _ => Err("two operands of one width".into()),
        }
    }
    fn eval(&self, args: &[BitVec], out: &mut [BitVec]) {
        let s = BitVec::apply_bin(BinOp::Add, &args[0], &args[1]).unwrap();
        out[1] = BitVec::from_bool(BitVec::apply_cmp(CmpOpExt::Ult, &s, &args[0]).unwrap());
        out[0] = s;
    }
    fn known_bits(&self, args: &[KnownBits], out: &mut [KnownBits]) {
        // The sum's bits come from bitwright's own transfer.
        out[0] = KnownBits::apply_bin(BinOp::Add, &args[0], &args[1]).unwrap();
    }
}

let registry = Arc::new(Registry::builder().register(AddCf)?.build());
let add_cf = registry.id("x86.add_cf").unwrap();
let mut cx = Context::with_registry(ContextConfig::default(), registry);
let x = cx.symbol("x", Width::W32)?;
let y = cx.symbol("y", Width::W32)?;
let out = cx.ext(add_cf, &[x, y])?;
assert_eq!(cx.display(out[1]).to_string(), "@x86.add_cf[1](x, y)");
// Parsed back in the same context, it is the same node.
assert_eq!(cx.parse("@x86.add_cf[1](x, y)", &Default::default())?, out[1]);
// 0xffffffff + 1 wraps to 0 and sets the carry.
let env = [
    (SymbolKey::from("x"), BitVec::ones(Width::W32)),
    (SymbolKey::from("y"), BitVec::one(Width::W32)),
];
let v = cx.eval(&out, &env[..])?;
assert!(v[0].is_zero() && v[1].is_ones());
Ok::<(), Box<dyn std::error::Error>>(())
}

The simplifier treats an extension node as an atom: no rule or pass looks inside it (an invertibility declaration is the one thing it uses, at equalities). It still simplifies the arguments and rebuilds the call, unless the operation’s traits() mark it opaque. Facts come from known_bits, and are exact when every argument is known. Constraints work through extension nodes like any other. With feature smtlib, an operation without an smtlib term exports as an uninterpreted function, and the script declares QF_UFBV. A solver’s unsat still proves equality, but a sat model may rely on values the real operation never produces.

An operation that must never be merged with another call on the same arguments (for example a read of a volatile resource) takes a fresh symbol as one of its arguments.

Memory

Lifted code reads and writes memory: spills to the stack, fields of a structure, a lookup table in read-only data. bitwright::memory turns loads and stores into the operators bitwright already has, at construction, so the simplifier, the facts, the prover and SMT-LIB export see nothing new.

A Memory maps addresses of one width to cells of another (bytes, usually), little- or big-endian for values wider than a cell. Stores make versions; a load reads a version:

  • each cell is read through the stores before it, newest first. A store at an equal address gives the cell; one at a different address is skipped; one whose address may be either becomes a select on the equality. Addresses with one base and constant offsets (sp - 8, sp - 4) are decided by their offsets, others by the builder or the facts;
  • what no store covers comes from the memory’s base: known contents where the address is in a region of them (a constant, or a table selected by the offset when the address is not constant), and otherwise an unknown cell, the symbol name.k. A later read of unknown memory at an address that may equal an earlier one selects the earlier cell when they are equal, so equal addresses always read equal values however they are written;
  • the cells of a load are put together again, and pieces of one stored value become that value: a spill reloaded is the spilled value.
#![allow(unused)]
fn main() {
use bitwright::memory::{Endian, Memory};
use bitwright::{BitVec, Context, ParseOptions, SymbolKey, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W64);
// A lookup table at 0x4000 (read-only data), the rest unknown.
let table: Vec<u8> = (0..16u8).map(|i| i.wrapping_mul(13) ^ 0x5a).collect();
let mut mem = Memory::new("mem", Width::W64, Width::W8, Endian::Little).with_bytes(0x4000, &table)?;
let m0 = mem.initial();

// A spill and a reload: the reload is the value.
let (slot, x) = (cx.parse("sp - 8", &o)?, cx.parse("x", &o)?);
let m1 = mem.store(&mut cx, m0, slot, x)?;
let x_again = mem.load(&mut cx, m1, slot, 8)?;
assert_eq!(x_again, x);

// A table lookup at an index the facts show in range, before the spill (after it, the
// lookup would depend on whether the spill hit the table): a table, no unknown memory.
let at = cx.parse("0x4000 + (i & 15)", &o)?;
let entry = mem.load(&mut cx, m0, at, 1)?;
assert!(mem.reads().is_empty());
let env = [(SymbolKey::from("i"), BitVec::from_u64(Width::W64, 3)?)];
assert_eq!(cx.eval(&[entry], &env[..])?[0].to_u64(), Some(u64::from(table[3])));

// A store through a pointer that may alias the slot: the reload depends on it.
let (p, y) = (cx.parse("p", &o)?, cx.parse("y", &ParseOptions::width(Width::W8))?);
let m2 = mem.store(&mut cx, m1, p, y)?;
let low = mem.load(&mut cx, m2, slot, 1)?;
assert_eq!(cx.display(low).to_string(), "select(sp - 8 == p, y, trunc<8>(x))");
Ok::<(), Box<dyn std::error::Error>>(())
}

The unknown cells are ordinary symbols, so a model of an expression that reads memory is a model of the memory it read. Memory::reads lists each read’s address and symbol, and Memory::bind evaluates the addresses in turn to give the symbols their values from a memory image, for evaluating an expression against a concrete memory.

SMT-LIB arrays import as memories (see SMT-LIB).

Lifted code

A disassembler or a lifter turns machine code into an IR; bitwright::lift reads three of them into expressions, so that what a block computes can be simplified, compared or proved:

  • p-code, Ghidra’s IR, one operation per line, varnodes (space, offset, size) as Ghidra prints them, or with a register name for the offset ((register, RAX, 8));
  • VEX, the IR of Valgrind and angr, as pyvex prints an IRSB;
  • LLVM IR functions (feature prove), with loads, stores and getelementptr over one flat memory.

A block becomes a Block: the registers it reads before writing them (symbols named after them), the final value of each register it writes, its stores, its conditional exits, and where it continues. Registers are read and written by byte range, so x86-64’s eax, ax, al and ah are parts of rax; memory is a memory, so a value stored and loaded back is the value. One block is read straight through: an exit is recorded, and the code after it runs as if the exit was not taken.

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::lift::{pcode, vex};
use bitwright::Context;

let mut cx = Context::new();
// An MBA, as pyvex prints the IRSB of `(x & y) * (x | y) + (x & ~y) * (~x & y)`.
let block = vex(
    &mut cx,
    "t0 = GET:I64(rdi)
     t1 = GET:I64(rsi)
     t2 = And64(t0,t1)
     t3 = Or64(t0,t1)
     t4 = Mul64(t2,t3)
     t5 = Not64(t1)
     t6 = And64(t0,t5)
     t7 = Not64(t0)
     t8 = And64(t7,t1)
     t9 = Mul64(t6,t8)
     t10 = Add64(t4,t9)
     PUT(rax) = t10",
)?;
let engine = bitwright::engine::Engine::builder()
    .builtin()
    .strategy(bitwright::engine::Strategy::deobfuscate().with_mba(Default::default()))
    .build()?;
let rax = engine.simplify(&mut cx, block.register("rax").unwrap())?;
assert_eq!(cx.display(rax.expr).to_string(), "rdi * rsi");

// A stack spill in p-code, Ghidra's register offsets: the reload is the spilled value.
let block = pcode(
    &mut cx,
    "(register, 0x20, 8) = INT_SUB (register, 0x20, 8) , (const, 0x8, 8)
     STORE (const, 0x1b1, 4) , (register, 0x20, 8) , (register, 0x38, 8)
     (register, 0x0, 8) = LOAD (const, 0x1b1, 4) , (register, 0x20, 8)
     (register, 0x0, 8) = INT_ADD (register, 0x0, 8) , (const, 0x1, 8)",
)?;
let rax = Engine::standard().simplify(&mut cx, block.register("reg_0x0").unwrap())?;
assert_eq!(cx.display(rax.expr).to_string(), "reg_0x38 + 1");
Ok::<(), Box<dyn std::error::Error>>(())
}

bitwright lift file.vex (or .pcode, .ll, or --from) prints each register a block writes, each store and each exit, deobfuscated; --synth also searches for a smaller equal expression. A call, a helper VEX leaves opaque (an unknown value, for a helper call such as a flag computation), a loop or an operation outside the front end’s subset is reported, not guessed.

tools/plugins/ has scripts for Ghidra (p-code), Binary Ninja (its low-level IL) and IDA Pro with Hex-Rays (the ctree) that hand the code at the cursor to the Python package. They are written against each tool’s documented API and are not tested, as none of the tools is available where bitwright is developed.

Simplifying

An Engine holds linked rules and a Strategy: an ordered list of phases, run bottom-up over each root and repeated for a few rounds until nothing changes. Build it once and share it (it is cheap to clone and Send + Sync); run it on any context.

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Strategy};
use bitwright::{Context, ParseOptions, Width};

let engine = Engine::builder()
    .builtin() // the built-in rules, vouched for by their proof ledger
    .strategy(Strategy::standard())
    .build()?;
let mut cx = Context::new();
let e = cx.parse("((x & 0xff) | (x & 0xff00)) + (y ^ y)", &ParseOptions::width(Width::W32))?;
let out = engine.simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x & 65535");
assert!(out.changed);
Ok::<(), Box<dyn std::error::Error>>(())
}

Ways to simplify

The same expressions can be simplified in several ways, depending on what a tool needs:

ToUseSee
clean up compiler or lifter outputEngine::standard()below
simplify every value of every function, in a compilerStrategy::compile()below
undo obfuscation, MBA includedStrategy::deobfuscate() with the MBA service, or the command line’s simplifyDeobfuscation and MBA
run only some passes, or more roundsa Strategy of your own Phasesbelow
simplify under a path conditionRun::with_assumptionsConstraints
decide a condition without rewritingContext::prove, Context::factsFacts and proofs
rewrite a target’s own idiomsrules of your own, linked with their proof ledgerWriting rules
find a smaller form the directed engine missesthe equality-saturation searchEquality saturation
keep some rewrites out, or watch themHooks and Observersbelow
check a result independentlyan SMT-LIB equivalence querySMT-LIB
use a solver’s results, or simplify its termsSMT-LIB importSMT-LIB

Examples shows each of them on a task from reverse engineering.

Strategies

Strategy::standard() is fact folding, the built-in rules, the normal-form passes (linear arithmetic, xor forms, casts, equalities through invertible maps, comparisons, bitwise truth tables, demanded bits) and the rules again. Strategy::deobfuscate() adds the linear-MBA and bit-shuffle passes (see Deobfuscation and MBA). You can build your own from Phases.

A strategy of your own runs just the phases you name, in your order. One that only folds what the facts pin and collects linear sums cancels additive masking and leaves everything else as the lifter wrote it:

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Phase, Strategy};
use bitwright::{Context, ParseOptions, Width};

let unmask = Engine::builder()
    .builtin()
    .strategy(Strategy::new("unmask", vec![Phase::FactFold, Phase::Linear]))
    .build()?;
let mut cx = Context::new();
let e = cx.parse("(x + y) * 3 - 3 * y + ((z & 0xff) | (z & 0xff00))", &ParseOptions::width(Width::W32))?;
let out = unmask.simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x * 3 + (z & 255 | z & 65280)");
// The standard strategy merges the masks too.
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x * 3 + (z & 65535)");
Ok::<(), Box<dyn std::error::Error>>(())
}

Phase::Local runs the rules of the groups it names (the built-in groups are listed in the rule catalog), and Strategy::max_rounds bounds the rounds.

Each pass commits a rewrite only if it makes the expression DAG strictly smaller, counted over everything the call keeps alive, so shared subexpressions are never duplicated to “simplify” one user. Equal-size rewrites are never made, which is why passes and rules cannot fight. The one exception is Phase::Invert (see Invertibility): like a rule, it replaces a comparison by one of proper subterms of its operands, so it commits even when those operands stay alive for other users. Strategy::sharing can make the passes decide by each node’s own subexpressions instead (Sharing::Ignored, see Inside a compiler), and Strategy::max_region bounds how many nodes a pass examines to decide.

Budgets, allowances and deadlines

Every run is bounded. A Budget caps node visits, rule candidates, matcher steps, rewrites, new nodes, fact work, pass work, MBA solver calls and equality-saturation work; Run carries the per-call budget and optionally an Allowance, an account several calls share. Work is charged before it is done, so a call never spends past its limit:

#![allow(unused)]
fn main() {
use bitwright::engine::{Allowance, Budget, End, Engine, Exhausted, Run};
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let e = cx.parse("((x + 1) + 2) + (y - y)", &ParseOptions::width(Width::W32))?;
let engine = Engine::standard();
let out = engine.run(&mut cx, &[e], Run::default().with_per_call(Budget::default().with_rewrites(0)))?;
assert_eq!(out.roots[0].end, End::BudgetTerminated(Exhausted::Rewrites));
assert!(!out.roots[0].changed);
// Unchanged from how it was built (construction already folded `y - y`).
assert_eq!(cx.display(out.roots[0].expr).to_string(), "x + 1 + 2");

// One account for a whole analysis.
let mut account = Allowance::new(Budget::default().with_node_visits(10_000));
let out = engine.run(&mut cx, &[e], Run::default().with_allowance(&mut account))?;
assert_eq!(out.roots[0].end, End::Completed);
assert_eq!(cx.display(out.roots[0].expr).to_string(), "x + 3");
assert!(account.spent().node_visits > 0);
Ok::<(), Box<dyn std::error::Error>>(())
}

A root stopped by a budget still has a correct result (every rewrite made is kept); it is just not final, and a later call with more budget continues from it. A Deadline stops a run at a time your own Clock reports; bitwright never reads a clock itself. Budgets are the deterministic choice; a deadline makes results depend on timing.

Admission caps (Admission) decline roots that are too large before any work is done.

What the result tells you

Outcome::roots has one RootOutcome per input: the result, whether it changed, and how the run ended (Completed, BudgetTerminated, Declined). Outcome::stats counts everything: visits, memo hits, candidates, no-match and guard-false declines, rewrites, rejections, each pass’s calls and commits, and the MBA service’s answers. Unproductive work is counted like productive work.

Results are memoized in the context: simplifying the same node again (or a larger expression containing it) is answered from the memo, as long as the engine, the host hooks and the assumptions are the same.

Many expressions at once

Engine::run takes several roots and processes them one after another, in one context: work on shared subexpressions is done once, and a rewrite is weighed against every root that uses a node. A rewrite held back only because another root shared its subterms is reconsidered once every root is done, when those roots may no longer use them. For many independent expressions (every instruction of a function, a dataset of obfuscated expressions), Engine::run_each simplifies each root on its own, as a call with that root alone would, on threads. Each root is copied into a context of its own (Context::import, which also moves expressions between contexts in general), simplified there, and its result copied back. The results do not depend on the number of threads, and Outcome::stats sums the roots’:

#![allow(unused)]
fn main() {
use bitwright::engine::{Each, Engine, Strategy};
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let roots = [
    cx.parse("(x ^ y) + 2 * (x & y)", &o)?,
    cx.parse("(x | y) - (x & ~y)", &o)?,
    cx.parse("((a - b) >>u 31) ^ (((a ^ b) & (a ^ (a - b))) >>u 31)", &o)?,
];
let engine = Engine::builder().builtin().strategy(Strategy::deobfuscate()).build()?;
let out = engine.run_each(&mut cx, &roots, Each::default().with_threads(4))?;
let shown: Vec<String> = out.roots.iter().map(|r| cx.display(r.expr).to_string()).collect();
assert_eq!(shown, ["x + y", "y", "zext<32>(a <s b)"]);
Ok::<(), Box<dyn std::error::Error>>(())
}

Each takes a per-root budget, admission caps, assumptions (copied into each root’s context) and hooks that are Sync; observers, allowances and deadlines are run’s. On a target without threads the calling thread does all the work. The command line’s simplify --file does the same for a file of expressions, one per line.

Inside a compiler

A compiler simplifies every value of every function, and most of them are already simple, so it needs a cost per value that stays the same however large the function grows. Strategy::compile() is built for that. It runs the standard phases without demanded bits, in one round. Its passes decide as if each node’s subexpressions were used by that node alone (Sharing::Ignored), and look at most 64 nodes deep (Strategy::max_region). A decision therefore depends only on the node, never on which other values the call has, so every result is final and memoized. One call over every value of a function costs time in proportion to its size. A later call over the same values, or over values built on them, is answered from the memo.

The standard strategy weighs sharing across all the roots of a call. That gives smaller results, but some decisions must then be made again for every root, so a call over every value of a function costs time growing with its size. Strategy::compile() can leave a result larger where values share subterms: a rewrite may keep alive a subterm another value still uses. The compiler keeps its own use counts and decides what to replace.

Build values through the builder, not text, with the compiler’s value numbers as symbol keys, and reuse one context per function (Context::clear keeps its allocations, and Context::reserve makes room for a function you know the size of):

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Run, Strategy};
use bitwright::{BinOp, Context, Width};

let engine = Engine::builder().builtin().strategy(Strategy::compile()).build()?;
let mut cx = Context::new();
let w = Width::W64;
// The parameters %0 and %1.
let a = cx.symbol(0u64, w)?;
let b = cx.symbol(1u64, w)?;
let seven = cx.constant_u64(w, 7)?;
let v2 = cx.bin(BinOp::Add, a, seven)?; // %2 = add %0, 7
let v3 = cx.bin(BinOp::Sub, v2, seven)?; // %3 = sub %2, 7
let v4 = cx.bin(BinOp::And, v3, b)?; // %4 = and %3, %1
let v5 = cx.bin(BinOp::Or, v3, b)?; // %5 = or %3, %1
let v6 = cx.bin(BinOp::Add, v4, v5)?; // %6 = add %4, %5
let values = [v2, v3, v4, v5, v6];
let out = engine.run(&mut cx, &values, Run::default())?;
let shown: Vec<String> = out.roots.iter().map(|r| cx.display(r.expr).to_string()).collect();
assert_eq!(shown, ["#0 + 7", "#0", "#0 & #1", "#0 | #1", "#0 + #1"]);
// Every result is final: the same values again are answered from the memo.
let again = engine.run(&mut cx, &values, Run::default())?;
assert_eq!(again.stats.node_visits, 0);
assert_eq!(again.roots, out.roots);
Ok::<(), Box<dyn std::error::Error>>(())
}

Equal handles always mean equal values (see Semantics). A result that is already the expression of another value (%3 is %0 above) is that value, and the compiler can replace the uses of one with the other.

A compiler also knows things about values bitwright sees only as symbols: a parameter zero-extended from 32 bits, an aligned pointer, a load’s range, a value computed in another block. Context::declare_known states such knowledge as known bits of the symbol. Facts, proofs and the simplifier all use it:

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Strategy};
use bitwright::{BitVec, Context, KnownBits, ParseOptions, SymbolKey, Width};

let engine = Engine::builder().builtin().strategy(Strategy::compile()).build()?;
let mut cx = Context::new();
let w = Width::W64;
let o = ParseOptions::width(w);
let p = cx.symbol("p", w)?;
let n = cx.symbol("n", w)?;
// `p` is 8-byte aligned; `n` was zero-extended from 32 bits.
cx.declare_known(p, KnownBits::new(BitVec::from_u64(w, 7)?, BitVec::zero(w)).unwrap())?;
let high = BitVec::from_u64(w, 0xffff_ffff_0000_0000)?;
cx.declare_known(n, KnownBits::new(high, BitVec::zero(w)).unwrap())?;
let checks = cx.parse("((p & 7) == 0) & (zext<64>(trunc<32>(n)) == n)", &o)?;
let bump = cx.parse("(p + 8 & -8) + (n >>u 32)", &o)?;
let out = engine.simplify(&mut cx, checks)?;
assert_eq!(cx.display(out.expr).to_string(), "1:1");
let out = engine.simplify(&mut cx, bump)?;
assert_eq!(cx.display(out.expr).to_string(), "p + 8");
Ok::<(), Box<dyn std::error::Error>>(())
}

Without the declarations both stay as they are, apart from construction’s canonical forms. A declaration becomes part of the symbol’s meaning. A result is equal to its input for every value that agrees with the declarations, and SMT-LIB export states them as assertions. Declare right after creating a symbol: a declaration drops the facts and results the context has cached, since they may depend on it.

In a compiler covers the rest: translating instructions into expressions and results back, templates read at run time, and rewrites written in Rust.

One call over every value of a function is the fastest way to use it. A call per value as it is created (Engine::simplify) gives the same results, and values already simplified are answered from the memo. It costs more, because the passes’ own caches last one call. compile/* in bitwright-bench measures both shapes.

Hooks and observers

Hooks let the host veto a rewrite (admit) or a fold of a fact-known value into a constant (fold_known). An Observer receives an event per rule candidate and per rewrite; RuleCensus is one that counts them per rule.

#![allow(unused)]
fn main() {
use bitwright::engine::{By, Engine, Hooks, Run, RuleCensus};
use bitwright::{Context, Expr, ParseOptions, Width};

/// Keeps the linear pass out (say, because a later stage wants to see the original terms).
struct NoLinear;
impl Hooks for NoLinear {
    fn admit(&self, _: &Context, _before: Expr, _after: Expr, by: By<'_>) -> bool {
        by.name() != "linear"
    }
    fn revision(&self) -> u64 {
        1 // part of the memo key: change it whenever the policy changes
    }
}

let mut cx = Context::new();
let e = cx.parse("(x + 1) - 1 + y * 2 - y", &ParseOptions::width(Width::W8))?;
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x + y");

let mut census = RuleCensus::default();
let out = Engine::standard().run(
    &mut cx,
    &[e],
    Run::default().with_hooks(&NoLinear).with_observer(&mut census),
)?;
assert_eq!(cx.display(out.roots[0].expr).to_string(), "x + 1 - 1 + (y << 1) - y");
assert!(out.stats.hook_vetoes > 0);
assert_eq!(census.rules["core.arith::mul_pow2"].applied, 1);
Ok::<(), bitwright::Error>(())
}

Verification

Rules linked from a proof ledger are trusted; every application still passes cheap postconditions (the width is unchanged, the facts of the result are compatible with the input’s, the result is smaller in the termination order). Verify adds sampled evaluation of every application (Verify::strict() for tests), and rules linked without a ledger (allow_unproven) are always sampled.

Writing rules

Rules live in .bwr files: a header, then groups of rules and identities.

bitwright 1;

group my.bitwise {
    /// Absorption.
    #[example("p & (p | q)" => "p")]
    rule and_absorb_or<W>(x: W, y: W) { x & (x | y) => x }

    /// A mask that keeps every bit x can have is redundant.
    #[example("(p & 0x0f) & 0xff" => "p & 0x0f")]
    rule and_mask_redundant<W>(x: W, c: const W) {
        x & c => x
        if zero_bits(x, ~c)
    }
}

Rules

A rule rewrites left to right: pattern => template. It is generic over one to four width variables (<W>, <W, U>), which may be constrained: where W < U, U <= 2 * W, or moduli such as W % 8 == 0.

Parameters are declared with their width and what they may bind:

DeclarationBinds
x: Wanything of width W
c: const Wonly constants
s: sym Wonly symbols
n: nonconst Wanything but constants

The template may only use parameters that occur in the pattern.

Literals are typed from their context: 0, 42, -1, 0xff, true, false, and the rule-only literals ones, zero, one, smin_lit, smax_lit, lowmask(N) (the low N bits set) and bit(K). A width expression in value position (W - 1) is a literal of the inferred width. A literal that does not fit at some admitted width is a compile error, never truncated.

Guards follow if, combined with &&, || and !:

  • fact predicates, true only when the facts prove them: zero_bits(x, m) (every bit of the mask m is zero in x), one_bits(x, m), nonzero(x), disjoint(x, y) (no bit is one in both), and proves(a op b) for a comparison of parameters, literals and lets; on a float of a format, fp.not_nan<E, S>(x), fp.finite<E, S>(x) (neither a NaN nor an infinity) and fp.nonzero<E, S>(x) (not a zero of either sign). Fact predicates cannot be negated: the facts can only prove, never refute.
  • constant predicates on const parameters: is_pow2, is_lowmask, is_shifted_mask.
  • pure conditions: comparisons and arithmetic over const parameters, literals and lets.
  • one_use(e), for a parameter or a subterm written as in the pattern: the node it matched has no other user, so the rewrite frees it rather than adding nodes beside it ((x << k) & m => (x & c) << k if one_use(x << k)). It is about sharing, not values, so checking and proving take it as true; the engine answers it from the DAG’s use counts, and takes it as true when sharing is ignored.

Lets compute constants from constant parameters when the rule matches:

rule mul_pow2<W>(x: W, c: const W) {
    x * c => x << k
    if is_pow2(c)
    let k: W = ctz(c)
}

Termination

Every rule must make its input strictly smaller in a fixed termination order (a Knuth-Bendix order over the canonical form), and must not duplicate a variable more often than the pattern has it. The compiler checks this, so no set of rules can loop. A rewrite that does not decrease is an identity.

Identities

identity mul_add<W>(x: W, y: W, z: W) { x * (y + z) <=> x * y + x * z }

An identity is an unconditional equation: no guard, no let, no capture kinds, every parameter on both sides. The directed engine uses it left to right only if that direction decreases the order; the equality-saturation search uses identities in both directions.

Floating point

Rules can rewrite floating-point operations. They are written as in the text syntax, with the format as width expressions: fp.mul.rne<E, S>(x, y) is a product in the format with E exponent and S significand bits, whose operands have width E + S; a named format (fp.mul.rne.f32(x, y)) needs no width variables. A rounding-mode parameter, declared r: rm, stands for all five modes: the pattern binds it from the node it matches (fp.mul.r<E, S>(…)), and the template can use it. A rule has at most two.

Constants are written fp.zero, fp.nzero (−0), fp.inf, fp.ninf, fp.nan, fp.one, fp.none (−1), fp.two, fp.half, fp.min_normal, fp.min_subnormal and fp.max_finite (the largest finite value), each with its format: fp.one<E, S>, fp.inf.f64. The operations the builder makes from other operators (fp.neg, fp.abs, fp.copysign, fp.sub, fp.gt, fp.ge and the tests fp.isnan …) are written out as the builder writes them, so a pattern of them matches what it builds. They have no floating-point node of their own, so a pattern whose format is generic must also contain an operation that is one (it binds E apart from S), or name its format. x87’s load and store are not available in rules.

The fact predicates on floats let a rule use an identity that holds only on ordinary numbers: x · 1 is x except for a NaN operand (whose payload the product drops), so

rule mul_one<E, S>(x: E + S, r: rm) {
    fp.mul.r<E, S>(x, fp.one<E, S>) => x
    if fp.not_nan<E, S>(x)
}

rewrites fp.mul.rtz.f64(fp.from_sbv.rne.f64(i), 1.0), whose operand the facts know is no NaN, and leaves x · 1.0 of an unknown x alone.

#![allow(unused)]
fn main() {
use bitwright::check::{CheckConfig, Verdict, check_program};
use bitwright::rules::RuleProgram;

let src = "bitwright 1;
group float {
    /// A square is never negative: the sign of a product of equal signs is clear, and a NaN
    /// result is the canonical one.
    #[example(\"fp.abs.f64(fp.mul.rtz.f64(x:64, x:64))\" => \"fp.mul.rtz.f64(x:64, x:64)\")]
    rule abs_square<E, S>(x: E + S, r: rm) {
        fp.abs<E, S>(fp.mul.r<E, S>(x, x)) => fp.mul.r<E, S>(x, x)
    }

    /// Wrong: x · 1 is x only on values, a NaN operand's payload is lost.
    #[allow(BW0407)]
    rule mul_one<E, S>(x: E + S, r: rm) { fp.mul.r<E, S>(x, fp.one<E, S>) => x }
}";
let program = RuleProgram::compile(src).map_err(|e| e.render("float.bwr", src))?;
let checks = check_program(&program, &CheckConfig::default());
assert!(checks[0].is_sound() && checks[0].examples.is_empty());
let Verdict::Unsound(cx) = &checks[1].verdict else { panic!() };
// E = 2, S = 2, r = rne; x = 0xf:4: lhs = 0x7:4, rhs = 0xf:4 (−NaN becomes the NaN)
assert_eq!(cx.lhs.to_u64(), Some(0x7));
Ok::<(), String>(())
}

A rule may instead claim its sides equal as floats, every NaN one value: #[float_values] before the rule. It may then change a NaN’s payload or sign, so x · 1 = x needs no guard:

#[float_values]
rule mul_one_values<E, S>(x: E + S, r: rm) { fp.mul.r<E, S>(x, fp.one<E, S>) => x }

Its pattern must be a floating-point operation with a float result, whose format the checker and the SMT obligation compare the sides in. The engine applies such rules only when the strategy opts in (Strategy::with_float_values(true), bitwright simplify --float-values): for hosts that observe floats only as values, as a compiler’s fast-math does.

The checker checks a floating-point rule in every format whose widths it enumerates exhaustively (E and S up to 6, which is where formats are strangest) and at sampled wider ones, under every rounding mode of its mode parameters. It finds what holds almost everywhere: rounding a value to an integral one and then converting it to an integer is not the same as converting it directly in a format such as (2, 3), where 3.5 rounds to 4, which is past the largest finite value, so the rule needs where S <= E (a format in which every value with a large enough exponent is an integer).

Attributes and diagnostics

/// doc comments and #[example("input" => "output")] document a rule; examples are checked by the checker. #[allow(BW0402)] and #[allow(BW0407)] silence the two allowable lints (an unreachable pattern, a missing example); errors cannot be allowed. Every diagnostic has a stable code; bitwright explain BW0302 (or rules::explain) says what it means.

#![allow(unused)]
fn main() {
use bitwright::rules::{Level, RuleProgram};

let src = "bitwright 1;
group demo {
    #[example(\"p & (p | q)\" => \"p\")]
    rule absorb<W>(x: W, y: W) { x & (x | y) => x }
    rule no_example<W>(x: W, y: W) { x | (x & y) => x }
}";
let program = RuleProgram::compile(src).map_err(|e| e.render("demo.bwr", src))?;
assert_eq!(program.rules().len(), 2);
assert_eq!(program.rule("demo::absorb").unwrap().params.len(), 2);
// A note for the missing example, with its code and position.
let d = &program.diagnostics()[0];
assert_eq!((d.code, d.level), ("BW0407", Level::Note));

// A rule that could loop is rejected.
let looping = "bitwright 1;\ngroup g {\n    rule r<W>(x: W) { x => x + 0 }\n}\n";
let err = RuleProgram::compile(looping).unwrap_err();
assert!(err.render("g.bwr", looping).contains("BW0302"));
Ok::<(), String>(())
}

The built-in rules are listed in the rule catalog.

Checking rules

A rule is used only if it is proven. The checker (feature check, on by default) evaluates each rule’s obligation, if the guard holds then the pattern equals the template, on concrete values:

  • exhaustively at every admitted width assignment with widths up to 6, over every value of the parameters when they total at most 16 bits (and under every rounding mode of a rule’s rounding-mode parameters, so a floating-point rule is checked in every format up to (6, 6));
  • by sampling at 22 widths up to 512 bits (7, 8, 9, 12, 16, 31, 32, 33, 63, 64, 65, …), with boundary-biased values (0, 1, −1, the signed extremes, powers of two), half of them steered toward making the guard true.

The verdict is Sound only when there is no counterexample and the guard actually held in the exhaustive tier (and in the sampled tier, unless the exhaustive tier covered every admitted width). Otherwise it is Unsound with a counterexample, or Inconclusive with the missing tier named. This is testing, not proof: for proof at wide widths, ask the native prover (see Proving rules) or export the obligations to an SMT solver (see SMT-LIB).

#![allow(unused)]
fn main() {
use bitwright::check::{CheckConfig, Verdict, check_program};
use bitwright::rules::RuleProgram;

let src = "bitwright 1;
group demo {
    #[example(\"p & (p | q)\" => \"p\")]
    rule absorb<W>(x: W, y: W) { x & (x | y) => x }

    #[allow(BW0407)]
    rule wrong<W>(x: W, y: W) { x + y => x | y }
}";
let program = RuleProgram::compile(src).map_err(|e| e.to_string())?;
let checks = check_program(&program, &CheckConfig::default());
assert!(checks[0].is_sound() && checks[0].examples.is_empty());
match &checks[1].verdict {
    Verdict::Unsound(cx) => println!("counterexample: {cx}"), // W = 1; x = 0x1:1, y = 0x1:1: …
    other => panic!("{other:?}"),
}
Ok::<(), String>(())
}

Proving rules

CheckConfig::with_proofs(n) (bitwright check --prove, with n = 3) also proves each rule with bitwright’s native prover at up to n width assignments too wide to enumerate: widths 8, 32 and 64 for a rule over bit-vectors, binary16, binary32 and binary64 for a floating-point rule (each under every rounding mode), and every assignment of a rule over fixed widths. The prover bit-blasts the obligation, floating-point operations included, and decides it with its own SAT solver; a constant parameter under a guard (c a power of two) is split into the constants the guard admits when the whole question is too hard. A refutation is a counterexample, checked by evaluation; a rule with no assignment small enough to enumerate is Sound when every one of its proofs succeeds; the evidence counts the assignments proved and those left open (the solver has a conflict budget, with_proof_conflicts).

prove::rule asks about one assignment, with a certificate on request: the clauses and a DRUP proof, checked by an independent checker. prove::rule_all_widths proves every assignment up to a bound, and settles a rule of bitwise operations on its parameters and the constants 0 and all ones for every width at once: bit i of each side is one Boolean function of bit i of the parameters, the same at every position and every width, so width 1 decides it.

#![allow(unused)]
fn main() {
use bitwright::check::{CheckConfig, check_program};
use bitwright::prove::{self, Config, RuleOutcome};
use bitwright::rules::RuleProgram;

let src = "bitwright 1;
group demo {
    #[allow(BW0407)]
    rule wide(x: 64, y: 64) { (x ^ y) + 2 * (x & y) => x + y }

    #[example(\"p & (p | q)\" => \"p\")]
    rule absorb<W>(x: W, y: W) { x & (x | y) => x }
}";
let program = RuleProgram::compile(src).map_err(|e| e.to_string())?;
// At 64 bits there is nothing to enumerate: sampling alone would be inconclusive.
let checks = check_program(&program, &CheckConfig::default().with_proofs(3));
assert!(checks[0].is_sound());
assert_eq!(checks[0].evidence.proved_instances, 1);

// One rule at chosen widths, with a certificate that an independent checker verifies.
let absorb = &program.rules()[1];
match prove::rule(absorb, &[128], &Config::default().with_certificate(true))? {
    RuleOutcome::Proved(Some(certificate)) => certificate.check()?,
    other => panic!("{other:?}"),
}
// Bitwise operations only: width 1 settles every width.
assert!(prove::rule_all_widths(absorb, 64, &Config::default())?.every_width);
Ok::<(), Box<dyn std::error::Error>>(())
}

Asking in steps

prove::valid, valid_under and equal answer Proved, Refuted with a counterexample, or Unknown with the reason: Budget (the search reached max_conflicts or max_propagations; a larger budget may decide it), TooLarge (the circuit passed max_nodes, whatever the budget) or Unsupported. A caller that retries undecided questions under larger budgets keeps a prove::Question instead. The question is simplified, blasted and encoded once, and each solve goes on where the last one stopped, learned clauses included. Asking under 5,000 conflicts and then 15,000 more does what asking under 20,000 once does.

Proofs usually close early, and refutations take the search longest. Before searching, the prover evaluates the circuit on Config::samples assignments (256 by default). A refutation that many values give is found there, with no clauses built. Question::stats reports the work so far: nodes, samples, variables and clauses, conflicts, decisions and propagations. Propagations bound time more closely than conflicts do, since a conflict costs more propagation on a larger circuit.

#![allow(unused)]
fn main() {
use bitwright::prove::{Config, Limits, Outcome, Question, Unknown};
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let p = cx.parse("x * (y | 1) != 0x9e3779b1", &o)?;
let cfg = Config::default().with_samples(0).with_max_nodes(2_000_000);
let mut q = Question::valid(&mut cx, p, &cfg)?;
let mut budget = 10;
let answer = loop {
    match q.solve(&mut cx, Limits::conflicts(budget))? {
        Outcome::Unknown(Unknown::Budget { .. }) => budget *= 2,
        other => break other,
    }
};
assert!(matches!(answer, Outcome::Refuted(_))); // y = 0x9e3779b1 * x⁻¹, for any odd x
assert!(q.stats().conflicts > 10);
Ok::<(), bitwright::Error>(())
}

Rules as Lean theorems

bitwright lean rules.bwr (rules::lean::lean) writes the rules as Lean 4 theorems over BitVec, for every width: the width variables universally quantified (positive, and meeting the where constraints), the guard a hypothesis, the proof sorry for you to write. A proof in Lean covers every width at once, which neither the checker nor the prover can. --at 8 writes each rule instead at the widest widths up to 8 it admits, proved by bv_decide, whose certificates Lean’s kernel checks: an oracle that shares nothing with bitwright. Division is SMT-LIB’s (BitVec.smtUDiv); floating point, bit counts, pdep and pext, which Lean’s core BitVec lacks, leave a rule out with a comment.

$ bitwright lean --at 8 > rules8.lean && lean rules8.lean

At 8 bits bv_decide proves every built-in rule Lean can state but the distributivity and associativity of multiplication, which run out of its budget.

Proof ledgers

A proof ledger records, for every Sound rule, its content hash and the evidence. The engine links a program only together with a ledger that vouches for each of its rules; change a rule and its hash changes, so the ledger no longer vouches for it until you check it again.

#![allow(unused)]
fn main() {
use bitwright::check::{CheckConfig, check_program};
use bitwright::engine::{Engine, Run, RuleCensus, Strategy};
use bitwright::rules::{Ledger, RuleProgram};
use bitwright::{Context, ParseOptions, Width};

let src = "bitwright 1;
group my.rules {
    /// Whatever y is, x stays.
    #[example(\"(p | q) & (p | ~q)\" => \"p\")]
    rule and_or_complement<W>(x: W, y: W) { (x | y) & (x | ~y) => x }
}";
let program = RuleProgram::compile(src).map_err(|e| e.to_string())?;
let ledger = Ledger::from_checks(&check_program(&program, &CheckConfig::default()));
let text = ledger.render(); // check this in next to the rules

let engine = Engine::builder()
    .builtin()
    .program(program, &Ledger::parse(&text)?)
    .strategy(Strategy::standard().with_rule_groups(&["my.rules"]))
    .build()
    .map_err(|e| e.to_string())?;
let mut cx = Context::new();
let e = cx.parse("(a | b) & (a | ~b)", &ParseOptions::width(Width::W64)).map_err(|e| e.to_string())?;
let mut census = RuleCensus::default();
let out = engine
    .run(&mut cx, &[e], Run::default().with_observer(&mut census))
    .map_err(|e| e.to_string())?;
assert_eq!(cx.display(out.roots[0].expr).to_string(), "a");
assert_eq!(census.rules["my.rules::and_or_complement"].applied, 1);

// Without a ledger that vouches for it, a program does not link.
let again = RuleProgram::compile(src).map_err(|e| e.to_string())?;
assert!(Engine::builder().program(again, &Ledger::default()).build().is_err());
Ok::<(), String>(())
}

For experiments, EngineBuilder::allow_unproven(true) with unproven_program links rules without a ledger; each of their applications is then verified by sampling, and the statistics count them.

bitwright check rules.bwr --ledger rules.bwr.proof writes a ledger from the command line, and --against compares an existing one, which is how a CI job keeps ledgers fresh.

In a compiler

A compiler uses bitwright on its own IR, not on text: it translates instructions into expressions, simplifies every value of a function, translates results back, and may have rewrites of its own that the rule language cannot say. This chapter covers those pieces.

  • bitwright::translate:
    • Semantics says what an instruction computes.
    • Lowering turns a function into expressions.
    • Raise turns results back into instructions.
    • Template gives semantics as text, read at run time.
  • Strategy::compile(): the simplifier tuned for every value of every function. See Simplifying.
  • Context::declare_known: what the compiler knows of values defined elsewhere.
  • engine::Rewrite: rewrites written in Rust, linked into an engine at run time. check::rewrite tests them before they are trusted.

C, C++, Python and JavaScript have these pieces too, with host values as 64-bit integers and rewrites as callbacks: see their chapter.

Translating instructions

The compiler implements Semantics for its instruction type once: the value an instruction defines, and its expression over its operands. A Lowering holds one function’s translation: the expression of every value, and a symbol for every value defined outside it (a parameter, a load, a call result), with the known bits the compiler has of it, if any.

#![allow(unused)]
fn main() {
use bitwright::translate::{Lowering, Semantics};
use bitwright::{BinOp, BitVec, Context, Error, Expr, KnownBits, Width};

/// `dst = op a, b` over 64-bit values numbered by `u32`.
struct Inst {
    dst: u32,
    op: BinOp,
    a: u32,
    b: u32,
}

impl Semantics for Inst {
    type Value = u32;
    fn result(&self) -> Option<u32> {
        Some(self.dst)
    }
    fn lower(&self, lw: &mut Lowering<'_, u32>) -> Result<Expr, Error> {
        let a = lw.value(self.a, Width::W64)?;
        let b = lw.value(self.b, Width::W64)?;
        lw.context().bin(self.op, a, b)
    }
}

let mut cx = Context::new();
let mut lw = Lowering::new(&mut cx);
// %0 is a parameter the compiler knows is 16-byte aligned.
let aligned = KnownBits::new(BitVec::from_u64(Width::W64, 15)?, BitVec::zero(Width::W64)).unwrap();
lw.input(0, Width::W64, Some(aligned))?;
lw.lower_all(&[
    Inst { dst: 2, op: BinOp::And, a: 0, b: 1 },
    Inst { dst: 3, op: BinOp::Add, a: 2, b: 0 },
])?;
assert_eq!(lw.inputs().len(), 2); // %0, and %1 on first use
let v3 = lw.get(3).unwrap();
assert_eq!(lw.context().display(v3).to_string(), "($0 & $1) + $0");
Ok::<(), Box<dyn std::error::Error>>(())
}

Values defined outside get fresh symbols ($0, $1). Lowering::input_named gives one a name of the compiler’s choosing instead.

A compiler with many instructions, or one that loads its instruction set from a description file, can give semantics as text instead. A Template is an expression over named parameters, read at run time. It is compiled once for each combination of operand widths and then instantiated by copying, with no parsing:

#![allow(unused)]
fn main() {
use bitwright::translate::Template;
use bitwright::{Context, Width};

let umin = Template::new("select(a <u b, a, b)", &["a", "b"])?;
let pick = Template::new("select(c, a + 1, b)", &["c", "a", "b"])?;
// Check at startup instead of at the first use.
pick.check(&[Width::W1, Width::W32, Width::W32])?;

let mut cx = Context::new();
let (x, y) = (cx.symbol("x", Width::W32)?, cx.symbol("y", Width::W32)?);
let m = umin.instantiate(&mut cx, &[x, y])?;
assert_eq!(cx.display(m).to_string(), "select(x <u y, x, y)");
Ok::<(), Box<dyn std::error::Error>>(())
}

Raising results

After simplifying, Lowering::raise turns a result back into instructions through the compiler’s Raise. Equal expressions are equal values, so a node some host value already computes is that value, and raising doubles as value numbering. Only nodes no value computes are emitted, operands first:

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Strategy};
use bitwright::translate::{Lowering, Raise};
use bitwright::{BinOp, Context, Error, Expr, View, Width};

struct Emit(Vec<String>);
impl Raise for Emit {
    type Value = u32;
    fn emit(&mut self, _: &Context, _: Expr, node: &View, ops: &[u32]) -> Result<u32, Error> {
        let dst = 100 + self.0.len() as u32;
        self.0.push(match node {
            View::Const(c) => format!("%{dst} = const {c}"),
            View::Bin(op, ..) => format!("%{dst} = {op:?} %{}, %{}", ops[0], ops[1]),
            other => return Err(Error::Unsupported(format!("{other:?}"))),
        });
        Ok(dst)
    }
}

let engine = Engine::builder().builtin().strategy(Strategy::compile()).build()?;
let mut cx = Context::new();
let mut lw = Lowering::new(&mut cx);
let (x, y) = (lw.value(0, Width::W32)?, lw.value(1, Width::W32)?);
// %2 = (x ^ y) + 2·(x & y), obfuscated x + y.
let cx = lw.context();
let xor = cx.bin(BinOp::Xor, x, y)?;
let and = cx.bin(BinOp::And, x, y)?;
let one = cx.constant_u64(Width::W32, 1)?;
let twice = cx.bin(BinOp::Shl, and, one)?;
let v2 = cx.bin(BinOp::Add, xor, twice)?;
lw.define(2, v2)?;
let out = engine.simplify(lw.context(), v2)?;
let mut emit = Emit(Vec::new());
let v = lw.raise(out.expr, &mut emit)?;
assert_eq!(emit.0, ["%100 = Add %0, %1"]);
assert_eq!(v, 100);
Ok::<(), Box<dyn std::error::Error>>(())
}

Rewrites in Rust

Some rewrites need more than the rule language has: a target’s legality, a cost model, the compiler’s own analyses. A compiler writes those as an engine::Rewrite: a name, a group, a revision, and a function from a node to its replacement. The function works through a Site, which gives views of nodes, their facts, and construction. It is linked into an engine at run time, and runs in the rule phases whose strategy names its group, after the phase’s rules:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use bitwright::engine::{Engine, Rewrite, Site, Strategy};
use bitwright::{BinOp, Context, Expr, ParseOptions, View, Width};

/// `x % c` (unsigned) as `x` when the facts show `x < c`: a remainder the compiler's
/// range analysis would remove too.
struct NeedlessRem;
impl Rewrite for NeedlessRem {
    fn name(&self) -> &str { "acme.needless_rem" }
    fn group(&self) -> &str { "acme" }
    fn rewrite(&self, site: &mut Site<'_>, e: Expr) -> Option<Expr> {
        let View::Bin(BinOp::URem, x, c) = site.view(e)? else { return None };
        let c = site.as_u64(c)?;
        let hi = site.facts(x)?.urange().hi().to_u64()?;
        (hi < c).then_some(x)
    }
}

let engine = Engine::builder()
    .builtin()
    .rewrite(Arc::new(NeedlessRem))
    .allow_unproven(true) // sampled at every application; see below
    .strategy(Strategy::compile().with_rule_groups(&["acme"]))
    .build()?;
let mut cx = Context::new();
cx.symbol("b", Width::W8)?; // `b` is 8 bits wide
let e = cx.parse("urem(zext<32>(b), 1000)", &ParseOptions::width(Width::W32))?;
// The built-in rules and passes leave it.
let plain = Engine::builder().builtin().strategy(Strategy::compile()).build()?;
assert_eq!(plain.simplify(&mut cx, e)?.expr, e);
let out = engine.simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "zext<32>(b)");
Ok::<(), Box<dyn std::error::Error>>(())
}

A host rewrite simplifies: its results must be smaller (below). Rewrites that make code larger for a target, such as expanding an instruction the target lacks, belong in the compiler’s Raise.

Arbitrary code has no static proof, so the engine holds a host rewrite to more checks than a rule:

  • Termination. A result is committed only when it is smaller than the node in the order every rule decreases (tree size, then operator precedence), so rules and host rewrites together cannot cycle. A result that is not smaller is left, and Stats::host counts it.
  • Verification. A rewrite linked with EngineBuilder::rewrite (which needs allow_unproven, like an unproven rule program) is compared with the node at sampled points at every application. One found wrong is quarantined for the rest of the call.
  • Everything a rule gets. Its width is checked, the fact tripwire applies when it is on, and Hooks::admit can veto it (it is named as By::Rewrite). What it builds and the facts it asks for are charged to the call’s budgets.
  • Memoization. Its name, revision and trust enter the engine’s configuration hash. Bump Rewrite::revision whenever its results change, and keep it deterministic.

Checking a rewrite

Sampling at every application costs time in the compiler. check::rewrite tests a rewrite offline, in the compiler’s own test suite:

  • Where it applies. At every node of inputs the compiler gives, at every width they parse at, and on variations with other constants.
  • Values. Every result compared with its node, exhaustively when their symbols have few bits, at boundary-biased points otherwise.
  • The engine’s contract. Width, determinism, and the termination order.

A rewrite that passes can be linked with EngineBuilder::trusted_rewrite, without sampling:

#![allow(unused)]
fn main() {
use bitwright::check::{RewriteCheckConfig, RewriteFailure, rewrite};
use bitwright::engine::{Rewrite, Site};
use bitwright::{BinOp, Expr, View};

/// `(x ^ y) ^ y` as `x`.
struct XorTwice;
impl Rewrite for XorTwice {
    fn name(&self) -> &str { "acme.xor_twice" }
    fn rewrite(&self, site: &mut Site<'_>, e: Expr) -> Option<Expr> {
        let View::Bin(BinOp::Xor, a, y) = site.view(e)? else { return None };
        let View::Bin(BinOp::Xor, x, z) = site.view(a)? else { return None };
        if z == y { Some(x) } else if x == y { Some(z) } else { None }
    }
}
let report = rewrite(&XorTwice, &["(x ^ y) ^ y", "((a + 1) ^ 7) ^ 7"], &RewriteCheckConfig::default())?;
assert!(report.applications > 0 && report.exhaustive > 0);

/// `(x + c) - c` as `x + c`: wrong, and caught with a counterexample.
struct Wrong;
impl Rewrite for Wrong {
    fn name(&self) -> &str { "acme.wrong" }
    fn rewrite(&self, site: &mut Site<'_>, e: Expr) -> Option<Expr> {
        let View::Bin(BinOp::Add, a, _) = site.view(e)? else { return None };
        matches!(site.view(a)?, View::Bin(BinOp::Add, ..)).then_some(a)
    }
}
match rewrite(&Wrong, &["(x + 3) - 3"], &RewriteCheckConfig::default()) {
    Err(RewriteFailure::Differs { node, result, assignment }) => {
        assert!(!assignment.is_empty());
        println!("{node} => {result} differs at {assignment:?}");
    }
    other => panic!("{other:?}"),
}
Ok::<(), Box<dyn std::error::Error>>(())
}

This is testing, not proof: it covers the shapes the inputs exercise. Give inputs where the rewrite applies and where it nearly does.

Verifying compiler transformations

bitwright::transform (feature prove) proves that one program refines another under LLVM’s semantics: whatever the target may do, the source may do too. It reads peephole transformations in the syntax the Alive paper introduced (Lopes et al., PLDI 2015), and pairs of functions in a subset of LLVM IR (translation validation); it infers preconditions; and when a transformation is wrong it shows why, with inputs small enough to read, as LLVM IR constants.

A first proof, step by step

Say a compiler turns unsigned division by a constant into a shift. Write the rewrite in a file, div.opt: the source instructions, =>, and the target instructions. %x is an input, C a symbolic constant (any value, the same on both sides), and no type is given, so every width is checked:

Name: udiv by a power of two
%r = udiv %x, C
  =>
%r = lshr %x, log2(C)

bitwright prove div.opt answers with a counterexample at the smallest width where one exists:

INVALID      udiv by a power of two
the values differ (i2)
  %x = i2 -2
  C = i2 -1
source:
  %r = i2 0
target:
  %r = i2 1
source result: i2 0
target result: i2 1

0 valid, 1 invalid, 0 not decided

Values are printed as LLVM IR constants, which are signed: at two bits, -2 is 2 and -1 is 3. So 2 / 3 is 0, but log2(3) is 1 and 2 >> 1 is 1. The rewrite needs a condition on C. bitwright infer div.opt finds one:

Pre: isPowerOf2OrZero(C)
             udiv by a power of two  (weakest found; 14 valid and 258 invalid examples)

Zero is allowed because dividing by zero is undefined behavior in the source, and a target may do anything where its source is undefined. Add the condition as a Pre: line and prove again:

Name: udiv by a power of two
Pre: isPowerOf2OrZero(C)
%r = udiv %x, C
  =>
%r = lshr %x, log2(C)
$ bitwright prove div.opt
unknown      udiv by a power of two  (i64: no answer within 200000 conflicts)

Every width up to 32 was proved, but 64-bit division is too large a circuit for the solver’s default budget. The answer is unknown, never valid. Name the widths you care about with --widths, or give the type (%r = udiv i32 %x, C), or raise --conflicts:

$ bitwright prove --widths 1,2,3,4,5,6,7,8,16,32 div.opt
valid        udiv by a power of two  (i1; i2; i3; i4; i5; i6; i7; i8; i16; i32)

1 valid, 0 invalid, 0 not decided

bitwright prove exits with 0 only when every transformation in the file is valid, so a file of transformations can be checked in CI as it is. The same check from Rust, as a test (with include_str!("transforms.opt") in place of the text):

#![allow(unused)]
fn main() {
use bitwright::transform::{Config, Verdict, parse_transforms, verify};

let text = "
Name: udiv by a power of two
Pre: isPowerOf2OrZero(C)
%r = udiv %x, C
  =>
%r = lshr %x, log2(C)
";
let cfg = Config::default().with_widths(vec![1, 2, 3, 4, 5, 6, 7, 8, 16]);
for t in parse_transforms(text)? {
    let report = verify(&t, &cfg);
    // A report prints as `bitwright prove` does, counterexample included.
    assert!(matches!(report.verdict(), Verdict::Valid), "{report}");
}
Ok::<(), Box<dyn std::error::Error>>(())
}

The rest of this chapter describes each part: what “valid” means, the syntax, inference, whole functions, and floating point.

Refinement

A value is its bits and a poison flag. A program also has undefined behavior, a condition on its inputs. For every input (each may be a value or poison, unless noundef), the target is correct when the source has undefined behavior, or when the target has none and either the source is poison or both give the same value and the target is not poison:

∀ inputs, target choices. ∃ source choices.
    ub_src ∨ (¬ub_tgt ∧ (poison_src ∨ (¬poison_tgt ∧ value_tgt = value_src)))

The choices are what LLVM leaves open: each use of undef, the value of a frozen poison, the sign and payload of a NaN a floating-point operation returns, the sign of a zero under nsz, whether llvm.fmuladd fuses. The source’s are chosen for it; the target’s are any. The question is decided by counterexample-guided search over bitwright’s own SAT solver: a candidate input that no source choice found so far answers, then a source choice that answers it or a proof that none does.

An input is what a caller passes, and each program reads it through its own declarations: a target that adds noundef to a parameter has undefined behavior where a caller passes poison, and one that adds range or nofpclass sees poison where the argument is outside them, while the source’s attributes never apply to the target. (A poison argument to a noundef parameter of the source is the source’s undefined behavior, so such inputs need no answer.)

The semantics come from LLVM’s language reference: poison from nsw, nuw, exact, disjoint, nneg, samesign and trunc’s flags, from shifts past the width, from select on poison, and from range and nofpclass on parameters, return values and call sites; undefined behavior from division by zero or by poison, INT_MIN / -1 (a poison dividend included), a branch on poison, unreachable, a false llvm.assume, a poison argument to a noundef parameter or call argument, returning poison from a noundef function or call, and returning anything but a returned parameter.

Transformations

A transformation is an optional Name:, an optional Pre: condition, the source instructions, =>, and the target instructions. Registers the source reads without defining them are inputs; words in operands (C, C1) are symbolic constants, the same in both programs and never poison; operands may be constant expressions (-C, C1 + C2, log2(C), width(%x)). Types may be omitted: they are inferred, and each width left open is checked in turn (1 to 8, 16, 32 and 64 bits; half, float and double), smallest first, at most Config::max_types assignments (128, --max-types). A transformation with more is not decided: valid at the assignments checked is not valid, so the verdict is unknown until some types are given. Fast-math flags on a select or a phi make its type floating point.

#![allow(unused)]
fn main() {
use bitwright::transform::{Config, Mismatch, Verdict, parse_transforms, verify};

let text = "
Name: PR20186
%a = sdiv %X, C
%r = sub 0, %a
  =>
%r = sdiv %X, -C

Name: mul by a power of two
Pre: isPowerOf2(C)
%r = mul %x, C
  =>
%r = shl %x, log2(C)
";
let ts = parse_transforms(text)?;
let cfg = Config::default();
// Negating a quotient is not dividing by the negated divisor: INT_MIN / -1 is undefined.
match verify(&ts[0], &cfg).verdict() {
    Verdict::Invalid(cx) => {
        assert_eq!(cx.mismatch, Mismatch::TargetUb);
        assert_eq!(cx.types, "i2"); // the smallest width where it fails
    }
    v => panic!("{v:?}"),
}
assert!(matches!(verify(&ts[1], &cfg).verdict(), Verdict::Valid));
Ok::<(), Box<dyn std::error::Error>>(())
}

bitwright prove file.opt checks every transformation of a file:

INVALID      PR20186
the target has undefined behavior where the source has none (i2)
  %X = i2 -2
  C = i2 1
source:
  %a = i2 -2
  %r = i2 -2
target:
  %r = i2 -2
the target's undefined behavior
valid        mul by a power of two  (i1; i2; i3; i4; i5; i6; i7; i8; i16; i32; i64)

A counterexample is made small before it is shown: inputs that need not be poison are not, values are 0, 1 or −1 when they can be, and otherwise have as few significant bits as they can. select c, x, false is not and c, x:

INVALID      select to and
the target is poison where the source is not
  %c = i1 false
  %x = i1 poison

and freeze repairs it (%f = freeze %x, %r = and %c, %f). The target may use the source’s registers; those instructions stay in the target program. A source register that the target defines again must be refined as the result is, since the rewrite replaces it at its other uses too:

INVALID      redefined intermediate
the values of %a differ (i2)

for %a = add %x, 1, %r = and %a, 0 rewritten to %a = add %x, 2, %r = 0.

A counterexample lists the inputs and symbolic constants, then each named value of the source and of the target at those inputs, then the results. When the source makes choices (an undef, a freeze, a NaN), the source values shown are one of its executions, and none of its executions matches the target; the report says so on its last line.

The precondition may name the source’s registers as well as its inputs and constants (MaskedValueIsZero(%a, 1) with %a = mul %x, %y is about that product). Its language has &&, ||, !, signed comparisons (<, <=, …), unsigned ones (u<, u<=, …), and the predicates isPowerOf2, isPowerOf2OrZero, isSignBit, isMask, isShiftedMask, MaskedValueIsZero(%x, C) and the WillNotOverflow{Signed,Unsigned}{Add,Sub,Mul,Shl} family, each read as what it states about the values. Constant expressions have + - * & | ^ << >> u>> / /u %, ~ and -, and the functions abs, log2, width, trunc, zext, sext, umax, umin, smax, smin, countLeadingZeros, countTrailingZeros, popcount, udiv, urem, sdiv, srem.

Precondition inference

transform::infer (bitwright infer file.opt) finds the condition on the symbolic constants under which a transformation holds, from data, as Alive-Infer does (Menendez and Nagarakatte, PLDI 2017): the prover sorts values of the constants into valid and invalid ones (every value at small widths, a sample otherwise), a formula over a vocabulary of predicates is learned that excludes every invalid value and admits as many valid ones as it can, and the transformation is then verified with it at every width. A failure at another width adds that width’s values and learns again.

#![allow(unused)]
fn main() {
use bitwright::transform::{Config, Verdict, infer, parse_transforms};

let ts = parse_transforms(
    "%a = sdiv i8 %X, C
     %r = sub 0, %a
       =>
     %r = sdiv %X, -C",
)?;
let i = infer(&ts[0], &Config::default()).map_err(|e| e.to_string())?;
// Valid except where the negation or the division overflows.
assert_eq!(i.pre.as_deref(), Some("C != 1 && !isSignBit(C)"));
assert!(i.weakest);
assert!(matches!(i.report.unwrap().verdict(), Verdict::Valid));
Ok::<(), Box<dyn std::error::Error>>(())
}

Disjunctions come out too: two shifts by constants are one shift when either amount is past the width (the source is poison) or both and their sum are within it.

From the command line, each transformation gets a Pre: line (or none) and a summary: whether the condition is the weakest the examples allow, and how many valid and invalid values were seen. The condition is then verified at every width, so the budget limits of prove apply. Without the i8 above, PR20186 is inferred but not verified:

Pre: C != 1 && !isSignBit(C)
             PR20186  not decided: i16: no answer within 200000 conflicts

Give division and multiplication a type, or narrow --widths, when inferring.

Translation validation

bitwright tv before.ll after.ll checks that each function of the second file refines the function of the same name in the first; bitwright tv pair.ll checks that @tgt refines @src. Parameters are the inputs, matched by position.

Checking an optimizer

Take before.ll:

define i32 @clamp(i32 %x) {
entry:
  %neg = icmp slt i32 %x, 0
  br i1 %neg, label %zero, label %high
zero:
  ret i32 0
high:
  %big = icmp sgt i32 %x, 255
  %r = select i1 %big, i32 255, i32 %x
  ret i32 %r
}

define i32 @avg(i32 %a, i32 %b) {
  %x = and i32 %a, %b
  %y = xor i32 %a, %b
  %h = lshr i32 %y, 1
  %r = add i32 %x, %h
  ret i32 %r
}

define i1 @bits(i8 %x) {
  %m = and i8 %x, 12
  %c = icmp eq i8 %m, 12
  %n = and i8 %x, 4
  %d = icmp ne i8 %n, 0
  %r = and i1 %c, %d
  ret i1 %r
}

Optimize it and validate each function:

$ clang -x ir -O2 -S -emit-llvm before.ll -o after.ll
$ bitwright tv before.ll after.ll
valid        clamp
valid        avg
valid        bits

3 valid, 0 invalid, 0 not decided

clang made @clamp a smax and a umin with a range(i32 0, 256) return attribute, and @bits a single test of both bits. Each function is checked with its own attributes: those that change what a value may be are read on parameters, return values and call sites (noundef, range, nofpclass, and returned, which clang infers), ABI attributes (zeroext, signext) change nothing, and declarations and debug metadata are skipped. An attribute or metadata that the target declares and that bitwright does not model (!fpmath, !range, a call’s attribute group) makes the function unsupported, never silently ignored; so do pointers and memory, and a floating-point environment other than the default (strictfp, "denormal-fp-math" flushing, or relaxed-math attributes on the target). A function outside the subset is reported, and the others are still checked. To check one pass rather than a pipeline, run opt instead: opt -passes=instcombine -S before.ll -o after.ll.

A wrong rewrite fails. Here after.ll’s @avg has add nsw in place of add:

$ bitwright tv before.ll after.ll
valid        clamp
INVALID      avg
the target is poison where the source is not
  %a = i32 1
  %b = i32 -1
source:
  %x = i32 1
  %y = i32 -2
  %h = i32 2147483647
  %r = i32 -2147483648
target:
  %x = i32 1
  %y = i32 -2
  %h = i32 2147483647
  %r = i32 poison
source result: i32 -2147483648
target result: i32 poison
valid        bits

2 valid, 1 invalid, 0 not decided

The average never wraps as unsigned, so add nuw validates; it can wrap as signed.

tv reads functions in SSA form. clang’s -O0 output keeps each local variable in stack memory (alloca, load, store), which is outside the subset, so promote the variables to registers first:

$ clang -O0 -Xclang -disable-O0-optnone -S -emit-llvm f.c -o f0.ll
$ opt -passes=sroa -S f0.ll -o before.ll
$ clang -x ir -O2 -S -emit-llvm before.ll -o after.ll
$ bitwright tv before.ll after.ll

(-disable-O0-optnone keeps clang from marking the functions optnone, which opt would leave alone.) tools/tv-fuzz in the repository runs this check at scale: random functions optimized by clang, and mutants of the results, which bitwright must call invalid at inputs an independent interpreter confirms.

From Rust

transform::pairs reads the two modules (or one with @src and @tgt) into transformations, which verify checks as it checks the Alive syntax:

#![allow(unused)]
fn main() {
use bitwright::transform::{Config, Mismatch, Verdict, pairs, verify};

let before = "
define i32 @avg(i32 %a, i32 %b) {
  %x = and i32 %a, %b
  %y = xor i32 %a, %b
  %h = lshr i32 %y, 1
  %r = add i32 %x, %h
  ret i32 %r
}";
let after = "
define i32 @avg(i32 %a, i32 %b) {
  %x = and i32 %b, %a
  %y = xor i32 %b, %a
  %h = lshr i32 %y, 1
  %r = add nsw i32 %h, %x
  ret i32 %r
}";
let fixed = after.replace("add nsw", "add nuw");
let cfg = Config::default();
let bad = &pairs(before, Some(after))?[0];
match verify(bad, &cfg).verdict() {
    Verdict::Invalid(cx) => assert_eq!(cx.mismatch, Mismatch::TargetPoison),
    v => panic!("{v:?}"),
}
let good = &pairs(before, Some(&fixed))?[0];
assert!(matches!(verify(good, &cfg).verdict(), Verdict::Valid));
Ok::<(), Box<dyn std::error::Error>>(())
}

The subset

The subset: integers and half, bfloat, float, double and fp128; every arithmetic, bitwise, shift, comparison, conversion and select instruction with its flags, freeze, phi, and acyclic control flow (br, switch, ret, unreachable); the intrinsics umin, umax, smin, smax, abs, ctpop, ctlz, cttz, bswap, bitreverse, fshl, fshr, the saturating additions and subtractions, assume, fabs, copysign, sqrt, fma, fmuladd, minnum, maxnum, minimum, maximum, minimumnum, maximumnum, floor, ceil, trunc, round, roundeven, rint and nearbyint; the attributes noundef, range and nofpclass on parameters, return values and call sites, and returned. Uses must be dominated by their definitions and phis have one entry per edge, as LLVM’s verifier requires (a malformed function is a syntax error, not a proof). Loops, pointers, memory and calls to other functions are outside it.

Branching on poison is undefined, selecting on it is not, so branches that merge into a select are a valid rewrite and the reverse is not. With both functions in pair.ll:

define i8 @src(i8 %x, i1 %c) {
entry:
  br i1 %c, label %a, label %b
a:
  %y = add i8 %x, 1
  br label %m
b:
  %z = sub i8 %x, 1
  br label %m
m:
  %r = phi i8 [ %y, %a ], [ %z, %b ]
  ret i8 %r
}

define i8 @tgt(i8 %x, i1 %c) {
  %d = select i1 %c, i8 1, i8 -1
  %r = add i8 %x, %d
  ret i8 %r
}

bitwright tv pair.ll finds it valid. With @src and @tgt swapped, the branch is on a poison %c where the select only returned poison:

INVALID      src => tgt
the target has undefined behavior where the source has none
  %x = i8 0
  %c = i1 poison
source:
  %d = i8 poison
  %r = i8 poison
target:
  %z = i8 -1
  %y = i8 1
  %r = i8 -1
the target's undefined behavior

Each value is shown as the program computes it at the counterexample, whether its block runs or not.

Floating point

Values follow IEEE 754 in the default environment (round to nearest, subnormals kept). A NaN an operation returns has any sign, and a quiet bit and payload that are preferred (quiet, zero payload) or copied from a NaN operand (quieted or unchanged; fpext and fptrunc keep a payload’s high bits), as the language reference says; fneg, fabs and copysign only touch the sign. So fadd %x, -0.0 is %x (a NaN result may be the operand itself), and %x is not fmul %x, 1.0 (whose NaN sign is free). frem is fmod.

Of the fast-math flags, nnan and ninf make NaNs and infinities among the operands and the result poison, and nsz makes the sign of a zero operand or of a zero result insignificant (so fadd nsz %x, 0.0 is %x, and fadd nsz 1.0, -1.0 may be -0.0: adding nsz to an instruction is not a refinement). The flags whose meaning is which rewrites are allowed, reassoc, arcp, contract and afn, are not modeled: a transformation that needs them is checked against exact IEEE semantics and reported invalid.

Open floating-point types are checked as half, float and double. A file of the usual suspects:

Name: add -0.0
%r = fadd %x, -0.0
  =>
%r = %x

Name: add +0.0
%r = fadd %x, 0.0
  =>
%r = %x

Name: add +0.0, nsz
%r = fadd nsz %x, 0.0
  =>
%r = %x

Name: x - x
%r = fsub %x, %x
  =>
%r = 0.0

Name: x - x, nnan
%r = fsub nnan %x, %x
  =>
%r = 0.0

Name: double negation to multiply by 1
%a = fneg %x
%r = fneg %a
  =>
%r = fmul %x, 1.0
valid        add -0.0  (half; float; double)
INVALID      add +0.0
the values differ (half)
  %x = half -0.0
source:
  %r = half 0.0
target:
  %r = half -0.0
source result: half 0.0
target result: half -0.0
(the source's choices shown are one of its executions; none matches)
valid        add +0.0, nsz  (half; float; double)
INVALID      x - x
the values differ (half)
  %x = half 0xH7C00 (+inf)
source:
  %r = half 0xH7E00 (NaN)
target:
  %r = half 0.0
source result: half 0xH7E00 (NaN)
target result: half 0.0
(the source's choices shown are one of its executions; none matches)
valid        x - x, nnan  (half; float; double)
INVALID      double negation to multiply by 1
the values differ (half)
  %x = half 0xH7E00 (NaN)
source:
  %a = half 0xHFE00 (NaN)
  %r = half 0xH7E00 (NaN)
target:
  %r = half 0xHFE00 (NaN)
source result: half 0xH7E00 (NaN)
target result: half 0xHFE00 (NaN)

3 valid, 3 invalid, 0 not decided

−0.0 + +0.0 is +0.0, so adding +0.0 is not the identity until nsz makes the sign of a zero free. ∞ − ∞ is NaN, so x - x is 0.0 only when nnan makes a NaN result poison (ninf is not needed: a NaN from infinite operands is already covered by nnan). fneg only flips the sign, so negating twice gives %x back bit for bit, while fmul may return a NaN of either sign. Floating-point values print as LLVM writes them: a short exact decimal when there is one, else the hexadecimal encoding, with NaNs and infinities named in parentheses.

A rewrite that reassoc permits is still checked exactly, and fails:

Name: reassociate
%a = fadd reassoc float %x, %y
%r = fadd reassoc %a, %z
  =>
%b = fadd reassoc %y, %z
%r = fadd reassoc %x, %b
INVALID      reassociate
the values differ
  %x = float 1.0
  %y = float 1.0
  %z = float -33554432.0

Only rewrites that are exact under IEEE semantics, given the flags nnan, ninf and nsz, are proved.

Trusting a verdict

A verdict rests on bitwright’s encoding of LLVM’s semantics, its bit-blaster, and by default its simplifier and SAT solver. --certify (Config::certify) takes the last two out: every question is blasted as it is, and a proof counts only with a DRUP certificate that an independent checker verifies (slower; a certificate that does not check makes the verdict unknown). bitwright prove --smt and bitwright tv --smt (transform::smt, feature smtlib) print, for each type assignment, the final question the search answered, for another solver to confirm:

$ bitwright prove --smt --widths 1,2,8 div.opt | z3 -in
udiv by a power of two (i1)
unsat
udiv by a power of two (i2)
unsat
udiv by a power of two (i8)
unsat

Every answer of a valid transformation is unsat; for an invalid one whose source makes no choices, the failing assignment’s is sat. An assignment the search did not decide is echoed as NO QUESTION with the reason. The questions are bitwright’s encoding, so a solver checks the solving, not the semantics.

Limits

  • undef inputs are not modeled (inputs are values or poison). An undef in the source is a value per use; a value derived from undef has one value, not a value per use, which gives the source fewer behaviors than LLVM’s (never an unsound answer) and is refused in the target where it is used twice.
  • Wide multiplication and division, and double-precision arithmetic the two programs do differently, can exceed the SAT solver’s budget (--conflicts): the verdict is then unknown, never valid.
  • A transformation is checked at 128 type assignments at most (Config::max_types); with more, the verdict is unknown.
  • “Valid” is valid at each type assignment checked, not at every width: a rewrite that fails only at, say, 12 bits passes the default widths. Add such widths with --widths (Config::widths).
  • NaN payloads follow the language reference’s choices without the target-specific extra payloads (none on x86 and ARM).

Deobfuscation and MBA

Obfuscators hide simple operations behind mixed boolean-arithmetic (MBA) identities, such as x + y = (x ^ y) + 2·(x & y), and behind bit shuffles that take a value apart and put it back. Strategy::deobfuscate() adds two passes to the standard strategy for these (hash comparisons through invertible mixers are handled by the standard strategy; see Invertibility):

  • Linear MBA. A linear combination of bitwise functions of up to six atoms is determined by its values at the corners where every atom is 0 or all ones. The pass computes that signature and emits the cheapest equivalent form it knows, if that is smaller.
  • Shuffle. Values assembled from slices of other values (extracts, shifts, masks, concats, ors of disjoint parts) are traced bit by bit, and re-emitted as the rotation, byte swap, extension or plain slice they are.
#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Strategy};
use bitwright::{Context, ParseOptions, Width};

let engine = Engine::builder().builtin().strategy(Strategy::deobfuscate()).build()?;
let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
for (obfuscated, plain) in [
    ("(x | y) - (x & y)", "x ^ y"),
    ("(x & 0xff) + (x & 0xff00) - 2 * (x ^ y) + (x | y) * 2 - 2 * (x & y)", "x & 65535"),
    (
        "(zext<32>(trunc<8>(x)) << 24) | (zext<32>(extract<8, 8>(x)) << 16) \
         | (zext<32>(extract<16, 8>(x)) << 8) | zext<32>(extract<24, 8>(x))",
        "bswap(x)",
    ),
] {
    let e = cx.parse(obfuscated, &o)?;
    let out = engine.simplify(&mut cx, e)?;
    assert_eq!(cx.display(out.expr).to_string(), plain);
}
Ok::<(), Box<dyn std::error::Error>>(())
}

The MBA service (feature mba)

Nonlinear MBA (products of bitwise terms, for example) needs a solver. The MBA service lowers a fragment of an expression into a small standalone MbaExpr, asks an MbaSolver, and publishes the answer only through an evidence gate:

  1. the answer must have the input’s variables and width and agree with it at 64 points (always): zero, all ones, one, the signed minimum, the constants of both sides and their neighbours, single bit positions, and seeded random values;
  2. lifted back into the context, it must make the DAG smaller (checked before any proof, so an answer that would be rejected anyway is not proved);
  3. then evidence, in order: bitwright’s own certificates (below), a configured EquivalenceProver, the backend’s own Proved or Certified claim if MbaTrust::backend_certificates is set (off by default), or agreement at the sampled points if MbaTrust::sampled is set (off by default);
  4. the lifted answer must agree with the original at seeded values of its symbols, and pass the usual postconditions and host veto.

bitwright’s certificates are finite evaluation tests, each complete for its fragment:

  • linear MBA (only 0 and all-ones constants inside bitwise parts): the values at the corners where every variable is 0 or all ones;
  • polynomial MBA (sums of products of bitwise functions, any constants): the points where the set bits of all variables together lie in at most d bit positions, d the most bitwise factors in one product. For (x & y)·(x | y) + (x & ~y)·(~x & y) = x·y at 64 bits that is 18,337 points;
  • polynomials without bitwise operators: a small grid, {0, 1, 2} per variable for degree 2;
  • every assignment when the variables total at most 20 bits;
  • anything else through atoms: right shifts, casts and arithmetic under a bitwise operator are abstracted, paired between the two sides when they are provably equal (also as each other’s complements, x − 1 and −x, or when an atom’s bits follow a bitwise function of its definition’s leaves), and the two skeletons are compared by one of the tests above;
  • polynomials over symbols: two sides that expand to the same polynomial in variables, conjunctions and other bitwise subterms are equal whatever values those take;
  • carries: expressions built from + − ~ & | ^, small left shifts and products by small constants are compared bit by bit, over every carry that can reach each position, which decides them exactly ((y + y) & y & −y is 0 at every width);
  • when those leave the question open, a bitwise operation with a constant that reads only known bits of its other operand is read as arithmetic (−2·(x & 1) | 1 is −2·(x & 1) + 1, the low bit being known to be 0), and a variable is split into cases over a few of its bits: the bits it is read through a narrow mask at (x & 1), or the low bits bitwise operations with constants read (x ^ 1), each case proved on its own.

A test is sized before it runs and charged to the pass-work budget; one that does not fit is not started, and the node is asked again by a later call with more budget. NativeProver offers the same checks to hosts that use the MBA module directly.

The phase asks about the largest fragment first: the question at the top of a fragment is asked over the fragment as it is, and only when that answer is not taken are the pieces asked, inner ones first, and the top again over them. A piece’s answer can hide what only the whole shows (a relation between two atoms that rewrites one factor of a product identity).

Answers are cached (keyed by the lowered input, the solver’s and prover’s ids, the trust setting and the lowering version) in an MbaCacheStore you provide; MemoryCache is a bounded in-memory one.

#![allow(unused)]
fn main() {
use std::sync::Arc;
use bitwright::engine::{Engine, Strategy};
use bitwright::mba::{MbaConfig, MbaTrust, MemoryCache, SignatureSolver};
use bitwright::{Context, ParseOptions, Width};

let config = MbaConfig::default().with_trust(
    MbaTrust::default(), // the default: only evidence bitwright checks itself
);
let engine = Engine::builder()
    .builtin()
    .strategy(Strategy::deobfuscate().with_mba(config))
    .mba_solver(Arc::new(SignatureSolver)) // linear MBA only, and fast
    .mba_cache(Arc::new(MemoryCache::new(4096)))
    .build()?;
let mut cx = Context::new();
let e = cx.parse(
    "3 * (x & ~y) + 2 * (~x & y) + 5 * (x & y) - (x | y)",
    &ParseOptions::width(Width::W64),
)?;
let out = engine.run(&mut cx, &[e], Default::default())?;
assert!(out.stats.mba.calls + out.stats.mba.cache_hits > 0);
// 2·x + y + (x & y): the same function of x and y at every bit.
assert_eq!(cx.display(out.roots[0].expr).to_string(), "x + x + (x & y) + y");
Ok::<(), Box<dyn std::error::Error>>(())
}

NormalFormSolver is bitwright’s own solver beyond linear MBA. It reads bitwise functions exactly at every width, one truth table per bit class (the positions every constant read by a bitwise operator treats alike), so constants inside bitwise operators are no obstacle: (x ^ 0x10) + 2·(x & 0x10) is x + 0x10, and 3·(x & 0x55) + 3·(x & 0xaa) is 3·(x & 0xff). Products of bitwise terms are multiplied out symbolically, so they cancel exactly: (x & y)·(x | y) + (x & ~y)·(~x & y) is x·y, 2^63·(x·x + x) is 0 at 64 bits, and a sum that is a product comes out as one (x·(x & y) + y·(x & y) − (x & y)² is (x | y)·(x & y)). Subterms it cannot see through become atoms: arithmetic under a bitwise operator (unless it is secretly a bitwise function), right shifts and casts. Atoms with equal normal forms are one atom, so ((x ^ y) + 2·(x & y)) & z is (x + y) & z and ((x + y) & z) + ((x + y) & ~z) is x + y. A bitwise operation with a constant that reads only low bits of a polynomial that are known is arithmetic again, so (x + y)·(−2·(z & 1) | 1)² is x + y; and an atom’s definition that also appears outside it is replaced by the atom, so p + x + (x ^ 4) − ((x ^ 4) & p), with p any polynomial, is x + ((x ^ 4) | p). Forms are also rendered as sums of two bitwise functions (in forms that share subterms, so −6·~(x | (y | z)) and the majority (y & z) | (x & (y | z)) share y | z) or three, as a bitwise function times an odd constant, group by group when their atoms fall apart, with a bitwise function peeled off, and partly factored, so an obfuscation’s pieces come back as written: a 33-node obfuscation of (d·a | (a ^ d)) + (a ^ (a + a)) from the QSynth dataset comes back as exactly that. Atoms that depend on each other are seen through where a certificate proves it: ((y + 1) & (~y + ~y)) | y | x is x | y, and −(x & −x) is x | −x. A normal form over at most three atoms is also looked up in a precomputed table of the smallest expressions, so products the input has multiplied out come back: x·y + x + y + 1 is ~x·~y, and x² + 2·x·y + y² is (x + y)². A form from the table is used only when a certificate proves it; one that no certificate can decide is returned, if at all, as a sampled answer (NfOptions::synthesis turns the table off). Every answer is certified against its input before it is returned, and the evidence gate checks it again. Its work is bounded by MbaConfig::budget (in steps: normal forms, renderings and certificate evaluations); a question that needs more is answered Exhausted, counted, and left for a later call with more budget. It remembers its recent answers (NfOptions::memo), which saves time when the engine asks again and never changes an answer. It is the default solver (the command line’s simplify uses it too); SignatureSolver answers linear MBA only, at a fraction of the cost on code that is not obfuscated. Of the 76,080 expressions of CoBRA’s MBA datasets, it brings every one whose ground truth agrees with its input (75,737) to the ground truth’s size or less, 22,677 smaller; CoBRA itself reaches 85.0 % of them. See compare/ in the repository.

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Strategy};
use bitwright::mba::MbaConfig;
use bitwright::{Context, ParseOptions, Width};

// The default solver and trust: `NormalFormSolver`, bitwright's own evidence only.
let engine = Engine::builder()
    .builtin()
    .strategy(Strategy::deobfuscate().with_mba(MbaConfig::default()))
    .build()?;
let mut cx = Context::new();
let e = cx.parse("3 * (x & 0x55) + 3 * (x & 0xaa)", &ParseOptions::width(Width::W32))?;
let out = engine.simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "(x & 255) * 3");
Ok::<(), Box<dyn std::error::Error>>(())
}

A host can plug in a solver of its own through MbaSolver. ThreadedSolver wraps any solver with a hard wall-clock deadline per question (a late answer is abandoned and never cached). Trusting backend certificates means trusting the backend, so it is off by default: answers are accepted only on bitwright’s own certificates, which cover the fragments above, or on the proof of an EquivalenceProver the host supplies for the rest. A host that trusts its backend turns backend_certificates on.

Beyond MBA

Obfuscation rarely comes in one kind. The rest of the library handles what an obfuscator combines with MBA, all in the same run:

  • Opaque predicates. A condition whose value follows from the known bits or the ranges of its operands is folded by the facts, so the dead arm of a branch or a select goes (see Facts and proofs).
  • Encoded constants. Multiplications by an odd constant and its inverse, complements that cancel and sums that add up to a constant fold in the linear and rule passes.
  • Lifted flags. Sign, overflow, carry and zero flags spelled out bit by bit, and the flags of a floating-point compare, combine back into the one comparison a branch tests.
  • Hash checks. A comparison of an invertible keyed mix with a constant becomes one of the key with the preimage (see Invertibility).
  • Path conditions. Under assumptions, what the branches taken so far imply is used too, and each result names the assumptions it relied on.
#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Strategy};
use bitwright::mba::MbaConfig;
use bitwright::{Context, ParseOptions, Width};

let engine = Engine::builder()
    .builtin()
    .strategy(Strategy::deobfuscate().with_mba(MbaConfig::default()))
    .build()?;
let mut cx = Context::new();
// An always-true predicate picks an MBA of `x + y` over a decoy, and the choice is multiplied
// by 7 and by 7's inverse modulo 256.
let e = cx.parse(
    "select(((x | 1) & 1) == 1, (x ^ y) + 2 * (x & y), x * 0x34) * 7 * 0xb7",
    &ParseOptions::width(Width::W8),
)?;
let out = engine.simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x + y");
Ok::<(), Box<dyn std::error::Error>>(())
}

Examples has one section for each of them.

Synthesis (feature prove)

What no normal form or rule reduces can still have a small equal: synth::synthesize searches for one, whatever its shape. It enumerates expressions over the input’s variables, the constants it mentions and a few others (0, 1, all ones, the width less one, the sign bit), by size, keeping one expression per behavior on a set of sample points; one that behaves like the input everywhere sampled is proved equal by the native prover, and a counterexample becomes a sample and the search starts over. An answer is always proved, and always smaller (as a DAG) than the input. The search is bounded (7 nodes, 4 variables, 64 bits by default), so it suits the small residue a simplification leaves, not a whole function. bitwright simplify --synth runs it on the simplifier’s result.

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::synth::{Config, synthesize};
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let e = cx.parse("((x + y) & 1) ^ (x & 1)", &ParseOptions::width(Width::W32))?;
// The simplifier moves the mask outward; parity reasoning is the synthesizer's.
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "(x + y ^ x) & 1");
let s = synthesize(&mut cx, out.expr, &Config::default())?.unwrap();
assert_eq!(cx.display(s).to_string(), "y & 1");
Ok::<(), Box<dyn std::error::Error>>(())
}

Invertibility

Hash functions and mixers are usually chains of invertible steps: xor with a key, multiply by an odd constant, rotate, a data-dependent xorshift. bitwright proves when an expression is an injective or bijective function of one of its subexpressions, and the simplifier uses that at equalities:

  • cancel: f(x) == f(y) is x == y when f is injective (!= likewise);
  • move a constant across: f(x) == c is x == f⁻¹(c), or false when c has no preimage. A hash comparison becomes a plain equality.

Orderings are never touched: a bijection does not preserve them, so f(x) <u c says nothing about x.

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

// A keyed mixer: F_K(v) = S((v ^ K) * K) * K, with S(h) = h ^ ((h >>u 32) >>u (h >>u 60)).
const K: &str = "0xd3220fb78e33751f";
let fk = |v: &str, k: &str| {
    let m = format!("(({v} ^ {k}) * {k})");
    format!("(({m} ^ (({m} >>u 32) >>u ({m} >>u 60))) * {k})")
};
let engine = Engine::standard();
let mut cx = Context::new();
let o = ParseOptions::width(Width::W64);
let mut simplify = |src: String| -> Result<String, bitwright::Error> {
    let e = cx.parse(&src, &o)?;
    let out = engine.simplify(&mut cx, e)?;
    Ok(cx.display(out.expr).to_string())
};
// Equal hashes under one key: equal inputs.
assert_eq!(simplify(format!("{} == {}", fk("x", K), fk("y", K)))?, "x == y");
// Solved at a constant: F_K(x) is 0 at exactly one input, the key.
assert_eq!(simplify(format!("{} == 0", fk("x", K)))?, "x == 0xd3220fb78e33751f");
// Two keys ored are 0 only where both are: at two different inputs, so never.
let two = format!("({} | {}) == 0", fk("x", K), fk("x", "0xfa173d6f19f30f7d"));
assert_eq!(simplify(two)?, "0:1");
// Feeding the input back in destroys injectivity: F_K(x) ^ x is not cancelled.
let mix = |v: &str| format!("({} ^ {v})", fk(v, K));
assert_ne!(simplify(format!("{} == {}", mix("x"), mix("y")))?, "x == y");
// Nothing about orderings.
assert!(simplify(format!("{} <u 0x1000", fk("x", K)))?.contains("<u"));
Ok::<(), Box<dyn std::error::Error>>(())
}

The round trip F_K⁻¹(F_K(x)) = x needs no special support: the passes cancel it from the middle out (y * K * K⁻¹ is y, and S(S(y)) is y once the demanded-bits pass sees that S leaves bits 32 to 63 alone).

Layers

An expression is read as a chain of layers from a subexpression up to its root. A layer is one node that is an injective function of one operand when every other operand (a parameter) is fixed:

LayerKind
~v, -v, bswap(v), bitrev(v)bijective
v + k, v - k, k - v, v ^ k, rotl(v, k), rotr(v, k), any kbijective
v * k with k proved odd (a constant, or known bits, or an assumption)bijective
zext(v), sext(v), concat(v, k), concat(k, v)injective
an extension output declared invertible in an argumentas declared
several nodes whose value determines the inner value bit by bit (below)injective

Composing layers keeps injectivity, so a chain of them is injective in its innermost value. Cancelling needs the parameters to be the same on both sides (the same subexpressions): a map injective for each fixed parameter can collide across two parameters. Moving a constant across needs them to be constants, so the preimage is one.

A node combining two values that both depend on the inner one is not a layer in general, even when each is a bijection: f(x) ^ x, f_a(x) ^ f_b(x) and f_a(x) | f_b(x) all collide. The exception is a map that gives the inner value back bit by bit.

Maps recovered bit by bit

Over the nodes between a node and its inner value, the analysis tracks for every bit which bits of the inner value it can depend on (bitwise operators bit by bit, arithmetic from the bits below, shifts, rotations and casts re-indexed, nothing for bits the facts pin), and which of those flip it whatever the others are: its pivots. Bit k of x + c is flipped by x_k, because a carry only comes from below; bit k of x ^ (x >> 4) is flipped by x_k and by x_(k+4). The map is injective when every bit of the inner value can be recovered: it is the one dependency of some output bit not recovered yet, and a pivot of it. Those facts are computed with the inner value unknown, so the answer holds for every value, not only the ones it takes in context.

That covers xorshift steps like x ^ (x << 13); the mixer’s S, whose low half reads only the high half, which it leaves alone; T-functions, where bit i reads only lower bits; and block maps, such as a pointer encoding whose low bits are a bijection of the pointer’s low bits and whose high bits, once those are known, are one of its high bits:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Query, Truth, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::new(22)?);
let u = cx.parse("u", &o)?;
// Bit i of ((u << 1) | b) & h reads only bit i - 1 of u: a bijection of u, for every b and h.
let t = cx.parse("u - (((u << 1) | b) & h)", &o)?;
assert_eq!(cx.prove(Query::Bijective { e: t, of: u })?, Truth::True);
// Without the shift, bit i reads bit i: not proved (and not injective).
let t0 = cx.parse("u - ((u | b) & h)", &o)?;
assert_eq!(cx.prove(Query::Injective { e: t0, of: u })?, Truth::Unknown);
// Independent of `u`, or narrower than it: disproved.
let y = cx.parse("y * 3", &o)?;
assert_eq!(cx.prove(Query::Injective { e: y, of: u })?, Truth::False);

// A pointer encoding: 42 low bits, then 22 high bits corrected by the low half's carry.
let compact = |x: &str, [k, l, c, d, h]: [&str; 5]| {
    format!(
        "let dec = {x} ^ {l}; \
         let carry = zext<64>({k} >u ((dec + ({k} - {l} + {c})) & 0x3ffffffffff)); \
         let high = ({x} >>u 42) - (({x} >>u 41) & {h}) + carry - 1; \
         (high << 42) | ((dec + ({k} - {l} + {d})) & 0x3ffffffffff)"
    )
};
let mut cx = Context::new();
let o = ParseOptions::width(Width::W64);
let x = cx.parse("x", &o)?;
let e = cx.parse(&compact("x", ["k", "l", "c", "d", "h"]), &o)?;
assert_eq!(cx.prove(Query::Bijective { e, of: x })?, Truth::True);
// So a comparison with an encoded pointer is a comparison with the pointer.
let params = ["0x1234567890abcdef", "0x0f0f0f0f0f0f0f0f", "0x11", "0x22", "0x3ffff"];
let e = cx.parse(&format!("{} == 0x1234567812345678", compact("x", params)), &o)?;
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x == 0x1dcbac019f989879");
Ok::<(), Box<dyn std::error::Error>>(())
}

For a node combining two varying values, the analysis tries the nearest node every varying path goes through as the inner value first, then deeper ones: the mixer’s S((x ^ k) * k) is proved over (x ^ k) * k, since the multiplication mixes every bit of x below. To cancel f(a) == f(b), the two sides are first matched as one function of a pair of different subterms, as deep as their structure allows.

Query::Injective { e, of } and Query::Bijective { e, of } answer the question for any subexpression of, with every value that does not depend on it held fixed. Like every proof, True is proved and Unknown is always possible; False is reserved for the cases with a simple witness (e does not depend on of, or is narrower than it). Under assumptions (prove_under), facts assumed about parameters count, and the answer reports the constraints it relied on: x * k is a bijection of x wherever k & 1 == 1 is assumed.

The analysis looks at no more than 256 nodes at a time, and at inner values of at most 128 bits; larger layers are simply not recognized. Maps that are bijective for reasons it does not model, such as a GF(2)-linear map with cyclic dependencies (x ^ rotl(x, 5) ^ rotl(x, 9) at 32 bits), are not recognized either.

Linear maps

A value mixed with rotations and shifts of itself by xor (x ^ rotl(x, 3) ^ rotl(x, 5)) reads every bit several times, so no bit can be recovered first. Such a map is linear over GF(2): every output bit is the xor of some input bits (and a constant), M·x ⊕ c. It is injective exactly when the matrix M has full rank, and then its inverse is found by elimination. bitwright reads any region built from ^, ~, masks, shifts and rotations by constants, byte swaps and bit reversals this way when the bit-by-bit recovery fails. x ^ rotl(x, 3) is not injective (it maps x and ~x alike), and stays.

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let e = cx.parse("(x ^ rotl(x, 3) ^ rotl(x, 5)) == (y ^ rotl(y, 3) ^ rotl(y, 5))", &o)?;
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x == y");
let e = cx.parse("(x ^ rotl(x, 3)) == (y ^ rotl(y, 3))", &o)?;
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(out.expr, e);
Ok::<(), bitwright::Error>(())
}

The xor pass uses the same reading to simplify: it writes such an expression as the xor of rotations of x masked by the diagonals of M when that is smaller, so an involution applied twice is x.

Knowing more about data-dependent shifts

h ^ ((h >>u 32) >>u (h >>u 60)) shifts the high half by its own top four bits. When those bits are t, the high half is at most (t + 1) · 2^28 - 1, and shifting it right by t keeps it below 2^28, so S changes only bits 0 to 27. The facts for a right shift by a value’s own top bits (a >>u (a >>u k), however the two shifts are spelled) take this into account, so the demanded-bits pass can drop S under any mask or shift that ignores the low 28 bits:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W64);
let src = |n: u32| format!("let m = x * 0xd3220fb78e33751f; (m ^ ((m >>u 32) >>u (m >>u 60))) >>u {n}");
let e = cx.parse(&src(28), &o)?;
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x * 0xd3220fb78e33751f >>u 28");
// One bit lower it would be wrong (S can change bit 27), and nothing happens.
let e = cx.parse(&src(27), &o)?;
assert!(!Engine::standard().simplify(&mut cx, e)?.changed);
Ok::<(), Box<dyn std::error::Error>>(())
}

Extension operations

An extension operation declares, per output and argument, whether the output is injective or bijective in that argument with the others fixed, and supplies the inverse at a value. The declaration may depend on the known bits of the other arguments (a key known to be odd, say). Registration checks both at sampled points: invert must recover the argument from the output, and for a bijection find an argument for any value. The simplifier checks every answer of invert by evaluation before using it.

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::ext::{ExtOp, ExtSig, Invertible, Registry};
use bitwright::{BinOp, BitVec, CmpOpExt, Context, ContextConfig, KnownBits, Width};
use std::sync::Arc;

/// `rotl(x ^ k, 7)`: a bijection of `x` for every `k`.
struct Scramble;
impl ExtOp for Scramble {
    fn name(&self) -> &str { "acme.scramble" }
    fn signature(&self, args: &[Width]) -> Result<ExtSig, String> {
        match args {
            [x, k] if x == k && x.bits() == 32 => Ok(ExtSig::new(&[("s", *x)])),
            _ => Err("two 32-bit operands".into()),
        }
    }
    fn eval(&self, args: &[BitVec], out: &mut [BitVec]) {
        let t = BitVec::apply_bin(BinOp::Xor, &args[0], &args[1]).unwrap();
        out[0] = BitVec::apply_bin(BinOp::RotL, &t, &BitVec::from_u64(Width::W32, 7).unwrap()).unwrap();
    }
    fn invertible(&self, _output: u8, arg: u8, _args: &[KnownBits]) -> Invertible {
        if arg == 0 { Invertible::Bijective } else { Invertible::No }
    }
    fn invert(&self, _output: u8, _arg: u8, args: &[BitVec], value: &BitVec) -> Option<BitVec> {
        let t = BitVec::apply_bin(BinOp::RotR, value, &BitVec::from_u64(Width::W32, 7).unwrap()).ok()?;
        BitVec::apply_bin(BinOp::Xor, &t, &args[1]).ok()
    }
}

let reg = Arc::new(Registry::builder().register(Scramble)?.build());
let mut cx = Context::with_registry(ContextConfig::default(), reg.clone());
let op = reg.id("acme.scramble").unwrap();
let (x, y, k) = (cx.symbol("x", Width::W32)?, cx.symbol("y", Width::W32)?, cx.symbol("k", Width::W32)?);
let (a, b) = (cx.ext(op, &[x, k])?[0], cx.ext(op, &[y, k])?[0]);
let e = cx.cmp(CmpOpExt::Eq, a, b)?;
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x == y");
Ok::<(), Box<dyn std::error::Error>>(())
}

An operation can also name the operation that undoes it (ExtOp::inverse): a decryption for an encryption under one key, whose arguments are the encryption’s output and its other arguments. Once both are registered, the registry checks the round trip by sampled evaluation (refusing a wrong declaration), and the builder cancels it: dec(k, enc(k, x)) is x, while dec(j, enc(k, x)) stays.

Facts proved elsewhere

Some facts hold only on a finite domain and were proved by exhaustion elsewhere, for example “below 2^32, F_A(x) ^ F_B(x) equals the salt only at x = 0”. Such a fact is a rule, guarded by a fact predicate that proves the input fits:

bitwright 1;

group eos.facts32 {
    rule fact00<W>(x: W) where W == 64 {
        FA(x) ^ FB(x) == 0xd734e14a2ead11b5 => x == 0
        if proves(x <=u 0xffffffff)
    }
}

(with FA(x) and FB(x) written out in full). The checker cannot enumerate 2^32 inputs and reports it inconclusive, so no ledger vouches for it: link it with EngineBuilder::unproven_program and allow_unproven(true), which states that you vouch for it, and add its group with Strategy::with_rule_groups. It then fires only where the facts prove x <=u 0xffffffff (a zero extension, a mask, or an assumption).

Equality saturation (feature eqsat)

The simplifier only ever makes expressions smaller, one step at a time, so it cannot find a result that needs a detour: factoring x·y + x·z into x·(y + z) means applying distributivity backwards, and x·(y + 1) − x·y only cancels after it is expanded. The equality-saturation service searches for such alternatives in an e-graph, where every equation is applied without losing the original.

It is a search service, not a phase: you decide when to run it (at an output or checkpoint boundary, not in routine maintenance), on which roots, and with which allowance. What it finds is a candidate that your own profitability and verification decide to use.

#![allow(unused)]
fn main() {
use bitwright::eqsat::{Publication, SaturateConfig, Saturator, SearchRun};
use bitwright::{Context, ParseOptions, Width};

let (sat, report) = Saturator::builtin_groups(
    &["eqsat.distrib", "eqsat.cancel"],
    SaturateConfig::default(),
);
assert!(report.admitted.len() > 1);
let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let factor = cx.parse("x * y + x * z", &o)?;
let cancel = cx.parse("x * (y + 1) - x * y", &o)?;
let out = sat.search(&mut cx, &[factor, cancel], SearchRun::default())?;
assert_eq!(out.publication, Publication::Published);
assert_eq!(out.roots[0].candidate, Some(cx.parse("x * (y + z)", &o)?));
assert_eq!(out.roots[1].candidate, Some(cx.parse("x", &o)?));
Ok::<(), bitwright::Error>(())
}

Equations

Admitted equations are proven (by their ledger), unconditional, of one width, and in a conservative fragment: + − * & | ^, negation and complement, constants and symbols, widths up to 128. Identities are used in both directions; guard-free rules (cancellations like (x + y) − y → x) in their authored direction only. Other inputs are declined, never approximated.

The built-in equations come in groups: eqsat.assoc, eqsat.distrib (* over +), eqsat.distrib_and (& over | and ^), eqsat.distrib_or (| over &), eqsat.negation and eqsat.cancel. Choose the groups a search needs: some combinations never saturate (distrib_and with distrib_or rewrite each other’s results without end), and a search that does not saturate publishes nothing.

Publication

Each root is searched in its own e-graph, sharing only the allowance. A batch is published only if every root it searched saturated within its bounds: reaching the iteration cap, a budget or a deadline withholds the whole batch (Publication::Withheld), and the rest of the batch is not searched (those roots are reported as stopped, or as Skipped). A deadline already passed when the search starts withholds the batch even where the memo has the answer. Spent allowance stays spent. A candidate must be strictly smaller as a tree, no larger as a DAG, and agree with its input at sampled points.

Stable outcomes (saturated searches, admission declines) are memoized in the context, so asking again is free; incomplete searches are not, so more budget can succeed later.

#![allow(unused)]
fn main() {
use bitwright::eqsat::{Publication, RootEnd, SaturateConfig, Saturator, SearchRun};
use bitwright::{Context, ParseOptions, Width};

let (sat, _) = Saturator::builtin_groups(
    &["eqsat.distrib_and", "eqsat.distrib_or"],
    SaturateConfig::default(),
);
let mut cx = Context::new();
let e = cx.parse("a & b | a & c", &ParseOptions::width(Width::W32))?;
let out = sat.search(&mut cx, &[e], SearchRun::default())?;
assert_eq!(out.roots[0].end, RootEnd::IterationCap);
assert!(matches!(out.publication, Publication::Withheld { .. }));
assert_eq!(out.roots[0].candidate, None);
Ok::<(), bitwright::Error>(())
}

SMT-LIB (feature smtlib)

bitwright’s semantics are SMT-LIB’s, so every expression has an exact QF_BV counterpart, and any SMT-LIB 2.6 solver (z3, cvc5, bitwuzla, …) can evaluate it, check an equivalence, or prove a rule.

Export

smtlib::export writes a declare-const per symbol and a define-fun per node (so sharing is kept), then root0, root1, … for the roots. Operators QF_BV lacks (population count, leading and trailing zeros, byte swap, bit reverse, pdep, pext, the high half of a product, rotation by a variable count) are expanded exactly.

#![allow(unused)]
fn main() {
use bitwright::{Context, ParseOptions, Width, smtlib};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let a = cx.parse("(x ^ y) + 2 * (x & y)", &o)?;
let b = cx.parse("x + y", &o)?;
let script = smtlib::equivalence_query(&mut cx, a, b)?;
// `unsat` from a solver proves a = b at 32 bits.
assert!(script.ends_with("(assert (not (= root0 root1)))\n(check-sat)\n"));
Ok::<(), bitwright::Error>(())
}

smtlib::equivalence_query_under does the same under constraints. It asserts the constraints a rewrite relied on (its relies_on), so unsat proves the rewrite wherever they hold.

Import

smtlib::import reads a QF_BV subset: declarations and definitions without parameters, assertions, let, ite, =, distinct, the Boolean operators, and every QF_BV operator. Booleans become 1-bit expressions. Arrays from bit-vectors to bit-vectors (QF_ABV) become memories: select a load, store a store, resolved as the memory chapter describes (equality of arrays is refused). Anything else is an error, never a guess, and the reader is iterative and bounded, so hostile input cannot exhaust the stack.

#![allow(unused)]
fn main() {
use bitwright::{Context, smtlib};

let mut cx = Context::new();
let script = smtlib::import(
    &mut cx,
    "(declare-const x (_ BitVec 8))
     (define-fun twice () (_ BitVec 8) (let ((t (bvadd x x))) t))
     (assert (bvult twice #x10))",
)?;
let twice = script.definition("twice").unwrap();
assert_eq!(cx.display(twice).to_string(), "x + x");
assert_eq!(script.assertions.len(), 1);
Ok::<(), bitwright::Error>(())
}

Rule obligations

smtlib::rule_obligation(rule, widths) states a rule’s soundness at one width assignment: the parameters are free, the guard is asserted, and the two sides are asserted to differ. It is translated from the rule itself, not from expressions built through the canonicalizing constructors, so nothing stands between the rule and the proof. unsat proves the rule at those widths; sat comes with a counterexample. A rule with floating-point operations gives a QF_BVFP script, and the assignment lists its rounding-mode parameters after the widths, each as an index in RoundingMode::ALL (bitwright smt writes one obligation per mode).

#![allow(unused)]
fn main() {
use bitwright::rules::RuleProgram;
use bitwright::smtlib;

let src = "bitwright 1;
group g {
    #[allow(BW0407)]
    rule mask<W>(x: W, c: const W) { x & c => x if zero_bits(x, ~c) }
}";
let program = RuleProgram::compile(src).map_err(|e| e.to_string())?;
let rule = program.rule("g::mask").unwrap();
assert!(rule.admits(&[256]));
let script = smtlib::rule_obligation(rule, &[256]).map_err(|e| e.to_string())?;
assert!(script.contains("(declare-const |x| (_ BitVec 256))"));
Ok::<(), String>(())
}

From the command line, bitwright smt rules.bwr | z3 -in (or | bitwuzla) prints one answer per obligation; every answer must be unsat.

Solvers

bitwright never runs a solver itself. Its nightly tests run every kind of script it writes through both z3 and bitwuzla: exported expressions evaluated at random points, every built-in rule proved at widths up to 512, simplification results proved equal to their inputs (with extension calls as uninterpreted functions), and rewrites proved under the constraints they rely on. Both read a script from standard input:

bitwright smt rules.bwr | z3 -in
bitwright smt rules.bwr | bitwuzla

On the widest obligations (shifts and division at 256 bits) each gives up on a few, not always the same ones. A per-query time limit is -t:<ms> for z3 and --time-limit-per=<ms> for bitwuzla; either answers unknown when it runs out.

The command line

bitwright-cli builds a bitwright binary: simplify for anyone with an expression to read, and the other commands for rule authors. Reports go to standard output (also when a check fails); errors, including a rule file’s compile diagnostics, go to standard error. A command exits with 0 on success, 1 when a check fails (or lint finds errors), and 2 on bad usage or unreadable input. -- ends the options, for an expression starting with -.

CommandWhat it does
bitwright check rules.bwrChecks every rule’s soundness and examples; exits 1 unless every rule is sound and every example holds. --thorough checks more widths and bits exhaustively; --prove also proves each rule with the native prover at widths too wide to enumerate (8, 32 and 64; binary16, 32 and 64); --ledger out.proof writes the proof ledger; --against rules.bwr.proof compares with an existing ledger.
bitwright lint rules.bwrPrints every diagnostic, rendered with its position.
bitwright smt rules.bwrPrints each rule’s soundness obligation as SMT-LIB, separated by (reset), at every admitted assignment of the widths 8, 32 and 64; a rule admitted at none of those gets three other admitted assignments. --widths 13 (one width per width variable) and --rule group::name narrow it. A rule with no obligation prints a line the solver echoes (SKIPPED …) and the command exits 1.
bitwright catalog [rules.bwr]A Markdown catalog of the rules: the built-in rules without a file.
bitwright lean [rules.bwr] [--at 8]The rules as Lean 4 theorems over BitVec: for every width with sorry proofs, or with --at at fixed widths proved by bv_decide (see Checking rules).
bitwright prove opt.txtVerifies compiler transformations in the Alive syntax (Pre:, source, =>, target): each target must refine its source under LLVM’s semantics of poison, undefined behavior and floating point, at every width left open; a counterexample shows small inputs and each side’s values as LLVM IR constants. --widths 8,32 and --conflicts narrow it. Exits 1 unless every transformation is valid. See Verifying compiler transformations.
bitwright infer opt.txtInfers each transformation’s precondition over its symbolic constants and verifies it.
bitwright lift block.vexReads lifted code (Ghidra p-code, VEX as pyvex prints it, an LLVM IR function; `–from pcode
bitwright tv before.ll after.llTranslation validation: each function of the second file must refine the function of the same name in the first (or @tgt refine @src of one file), for a subset of LLVM IR.
bitwright explain BW0302What a diagnostic code means and how to fix it.
bitwright simplify '<expr>'Simplifies an expression (--width 32), deobfuscating: the rules, the normal-form passes and the MBA service with the native normal-form solver, every answer proved by bitwright itself; --standard runs only the rules and the standard passes. Each --assume '<predicate>' adds a 1-bit constraint; a result that relies on constraints is followed by # relies on 0, 2 (their positions among the --assume options). Each --rules my.bwr adds a rule file’s rules after the built-in ones, vouched for by the ledger my.bwr.proof next to it if there is one, else checked first (the command exits 1 unless every rule is sound). --file exprs.txt simplifies each line of a file on its own, on --jobs threads.

Simplifying from the shell

$ bitwright simplify --width 64 -- '(x & y) * (x | y) + (x & ~y) * (~x & y)'
x * y
$ bitwright simplify --width 8 -- '(x ^ 0xa5) * 0x3d * 0xf5'
(x ^ -91) * 97
$ bitwright simplify --width 32 -- '((a - b) >>u 31) ^ (((a ^ b) & (a ^ (a - b))) >>u 31)'
zext<32>(a <s b)
$ bitwright simplify --width 32 --assume 'x <u 16' --assume 'y == 3' -- '(x & 0xf0) + y * x'
x * 3    # relies on 0, 1
$ bitwright simplify --width 64 -- 'fp.div.rne.f64(x, 0x4010000000000000)'
fp.mul.rne.f64(x, 0x3fd0000000000000)

The expression is in the text syntax, floating-point operations included; symbols have the --width (64 by default) unless they say otherwise (b:8).

--file simplifies a file of expressions, one per line (- reads standard input), each on its own and on up to --jobs threads (all the machine runs at once by default). It prints one line per line read, blank lines and # comments as they are:

$ cat exprs.txt
# from the obfuscated function
(x ^ y) + 2 * (x & y)
(x | y) - (x & ~y)
$ bitwright simplify --width 32 --file exprs.txt --jobs 8
# from the obfuscated function
x + y
y

Rules of your own join the built-in ones with --rules; bitwright check my.bwr --ledger my.bwr.proof once saves checking them on every call:

$ cat my.bwr
bitwright 1;
group mine {
    /// Every bit is set in x or in ~x.
    #[example("popcnt(p:8) + popcnt(~p:8)" => "8:8")]
    rule popcnt_complement<W>(x: W) { popcnt(x) + popcnt(~x) => W }
}
$ bitwright simplify --width 32 --rules my.bwr -- 'popcnt(x) + popcnt(~x)'
32:32

Rule files

A typical workflow for a rule file:

bitwright lint my.bwr
bitwright check my.bwr --ledger my.bwr.proof
bitwright smt my.bwr | z3 -in | sort | uniq -c      # every line should be `unsat` (or `| bitwuzla`)

and in continuous integration:

bitwright check my.bwr --against my.bwr.proof

which fails when a rule changed without its ledger being regenerated, when a rule is not sound, or when an example stopped holding.

C, C++, Python and JavaScript

bitwright can be used from C, C++, Python and, compiled to WebAssembly, JavaScript. The bindings cover what a host needs to hand its expressions to bitwright and read the results back:

  • building, parsing, printing and inspecting expressions, at any width from 1 to 512 bits;
  • IEEE 754 floating point in any format and under every rounding mode, on the bit-vectors that hold the encodings;
  • evaluation and substitution;
  • facts (known bits, and ranges with a stride) and tri-state proofs, including invertibility, under assumptions, with the assumptions each answer relies on;
  • simplification with the standard engine or the deobfuscation engine (the command line’s simplify: the MBA service with the native solver, on bitwright’s own evidence), with budgets, and with rule files of your own, linked with their proof ledgers;
  • SMT-LIB export and import;
  • proofs of equivalence by the native prover, synthesis, and (Python) equality saturation;
  • memory (loads and stores as expressions) and lifted code (p-code, VEX, LLVM IR);
  • verifying compiler transformations: transformations in the Alive syntax, translation validation of LLVM IR, precondition inference;
  • engines that refuse rewrites by rule or pass name, and (Python) the trace of a run’s rewrites;
  • in a compiler: the compile strategy, declared known bits, host rewrites as callbacks with their checker, semantics templates, lowering a function into expressions and raising results back, and the counters of a run.

Extension operations, host hooks (Hooks) as callbacks, deadlines, allowances, custom strategies and other MBA backends are Rust only. The bindings have the version of the crate they are built with, and give the same results. JavaScript has a string API: text in, text out.

Python

The bitwright package is built from bitwright-py/ with maturin, which needs a Rust toolchain; one wheel serves CPython 3.10 and later.

pip install ./bitwright-py
import bitwright as bw

cx = bw.Context()
x, y = cx.symbols("x y", 64)

# Operators build expressions; an int is a constant of the other operand's width.
e = (x & y) + (x | y)
assert str(e.simplify()) == "x + y"

# Text, and the deobfuscation engine for MBA. `bw.simplify` is the command line's `simplify`.
mba = cx.parse("(x & y) * (x | y) + (x & ~y) * (~x & y)")
assert str(bw.Engine.deobfuscate().simplify(mba)) == "x * y"
assert bw.simplify("(x & y) * (x | y) + (x & ~y) * (~x & y)") == "x * y"

# Values are Python ints, exact at any width.
assert (x * 3 + 1).eval(x=2**64 - 1) == 2**64 - 2
w = cx.symbol("w", 256)
assert (w + 1).eval(w=2**256 - 1) == 0

# Facts, proofs, assumptions.
b = cx.symbol("b", 8)
f = ((b & 0xF0) | 1).facts()
assert (f.known_zero, f.known_one, f.umin, f.umax) == (0x0E, 0x01, 1, 0xF1)
assert f.ustride == 16  # the values are 1, 17, 33, ..., 241
small = bw.Assumptions(b.ult(16))
assert (b & 0xF0).eq(0).prove() is None  # not decided
assert (b & 0xF0).eq(0).prove(small) is True
out = bw.Engine().run([b & 0xF0], assumptions=small)[0]
assert str(out.expr) == "0:8" and out.relies_on == (0,)  # holds where assumption 0 does

# Invertibility: a keyed mixer is a bijection of its input.
assert ((b ^ 0x5A) * 0x1D).prove_injective(b, bijective=True) is True

Structurally equal expressions of a context are one node, so == on expressions is identity and expressions can be dictionary keys. Comparisons that build a 1-bit expression are methods (eq, ne, ult, ule, ugt, uge, slt, sle, sgt, sge), since < could be signed or unsigned; an expression has no truth value (if x.ult(y): raises TypeError: ask prove()). >> is the logical shift (ashr is the arithmetic one), // and % are unsigned, and an int operand must fit the width: 0 <= v < 2**w, or a negative value in two’s complement down to -2**(w - 1). kind, op, children, value and name inspect a node.

Rules of your own are checked once, and linked with their proof ledger:

import bitwright as bw

rules = """bitwright 1;
group my.rules {
    rule and_or_complement<W>(x: W, y: W) { (x | y) & (x | ~y) => x }
}"""
ledger = bw.check_rules(rules)  # raises RuleError with a counterexample if a rule is unsound
engine = bw.Engine(rules=[(rules, ledger)])

cx = bw.Context()
e = cx.parse("(a | b) & (a | ~b)", 64)
assert str(engine.simplify(e)) == "a"

# Budgets bound the work; a stopped result is still correct, just not final. (A result the
# context already holds, like `e`'s now, costs nothing.)
fresh = cx.parse("(p | q) & (p | ~q)", 64)
stopped = engine.run([fresh], budget=bw.Budget(node_visits=0))[0]
assert stopped.end == "budget" and stopped.limit == "node visits"
assert engine.run([e], budget=bw.Budget(node_visits=0))[0].end == "completed"

# Many independent expressions: each on its own, on threads, with the interpreter released.
exprs = [cx.parse(t, 64) for t in ["(x ^ y) + 2 * (x & y)", "(x | y) - (x & ~y)"]]
out = bw.Engine.deobfuscate().run_each(exprs, threads=4)
assert [str(o.expr) for o in out] == ["x + y", "y"]

Errors are exceptions: ParseError for text, WidthError for width rules, RuleError for rule files and ledgers, BitwrightError (their base) for the rest, ValueError for an int that does not fit and TypeError for an operand that is not an expression or an int. Contexts and engines may be shared between threads: a context serializes the operations on it, and simplification releases the interpreter. Engine.run_each simplifies many expressions each on its own on threads of its own (as Engine::run_each in Rust; bw_engine_run_each in C, run_each in C++). The package is typed (py.typed).

C

bitwright-ffi/ builds libbitwright as a shared and a static library, declared by bitwright-ffi/include/bitwright.h:

cargo build --release -p bitwright-ffi
cc -I bitwright-ffi/include app.c -L target/release -lbitwright

(The static library also needs the system libraries Rust’s standard library uses: -lpthread -ldl -lm on Linux.)

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "bitwright.h"

static void check(bw_status s) {
    if (s != BW_OK) {
        fprintf(stderr, "bitwright error %d: %s\n", s, bw_last_error());
        exit(1);
    }
}

int main(void) {
    bw_context *cx = bw_context_new();
    bw_engine *engine = bw_engine_new(BW_PRESET_STANDARD);
    bw_expr e, s, b, k, f;
    char *text;

    assert(bw_abi_version() == BW_ABI_VERSION);
    check(bw_parse(cx, "(x & y) + (x | y)", 64, &e));
    check(bw_simplify(engine, cx, e, &s));
    check(bw_print(cx, s, 0, &text));
    assert(strcmp(text, "x + y") == 0);
    bw_string_free(text);

    /* Constructors, and evaluation: b * 3 at 8 bits, b = 100, is 300 mod 256. */
    bw_value v = bw_value_u64(8, 100), r;
    check(bw_symbol(cx, "b", 8, &b));
    check(bw_const_u64(cx, 8, 3, &k));
    check(bw_bin(cx, BW_MUL, b, k, &f));
    check(bw_eval(cx, f, &b, &v, 1, &r));
    assert(r.width == 8 && r.limbs[0] == 44);

    /* Errors are status codes, described by bw_last_error. */
    bw_status st = bw_bin(cx, BW_ADD, e, b, &f);
    assert(st == BW_ERR_WIDTH);
    assert(strstr(bw_last_error(), "widths differ") != NULL);

    bw_engine_free(engine);
    bw_context_free(cx);
    return 0;
}

The conventions, stated in full at the top of the header:

  • A fallible function returns a bw_status, BW_OK or an error code; bw_last_error() describes the last error on the thread. Output parameters are written only on success.
  • Objects come from a *_new function and go back to the matching *_free, which accepts NULL; strings the library returns go back to bw_string_free.
  • A bw_expr is a 64-bit handle, valid in the context that made it until the context is cleared or freed. A handle from another context, or from before a clear, is rejected, never mistaken for another node. 0 (BW_NULL_EXPR) is never a handle.
  • A bw_value holds up to 512 bits in eight little-endian 64-bit limbs; bits above the width must be zero. bw_value_u64 and bw_value_i64 make one.
  • Enumerations cross as int, with fixed values; an unknown value is an error. Assumptions and proofs report the constraints they rely on as a mask: bit i for constraint i below 63, bit 63 for any from 63 on.
  • A context or an assumption set is used by one thread at a time; an engine may be shared by any number of threads.

BW_ABI_VERSION changes whenever a declaration does; check bw_abi_version() against it when the library is loaded at run time. bitwright-ffi/examples/ has more: facts, proofs and assumptions from C, and a tour of the C++ wrapper.

C++

bitwright-ffi/include/bitwright.hpp is a header-only C++17 wrapper over the C API: objects own their C counterparts, errors are thrown as bitwright::Error (with the status code), and expressions are small values with operators. It links against the same library.

#include <cassert>

#include "bitwright.hpp"

namespace bw = bitwright;

int main() {
    bw::Context cx;
    bw::Expr x = cx.symbol("x", 64), y = cx.symbol("y", 64);

    // Operators build expressions; integers are constants of the other operand's width.
    bw::Engine engine = bw::Engine::standard();
    assert(engine.simplify((x & y) + (x | y)) == x + y);
    assert(engine.simplify(((x ^ 0x5a) + 3 - 3) ^ 0x5a) == x);
    bw::Expr mba = cx.parse("(x & y) * (x | y) + (x & ~y) * (~x & y)");
    assert(bw::Engine::deobfuscate().simplify(mba).str() == "x * y");

    // Facts, and proofs under assumptions.
    bw::Expr b = cx.symbol("b", 8);
    bw::Facts f = bw::facts((b & 0xf0) | 1);
    assert(f.known_zero == bw::Value(8, 0x0e) && f.ustride == 16);
    bw::Assumptions small;
    small.assume(b.ult(b.constant(16)));
    assert(bw::prove((b & 0xf0).eq(b.constant(0)), &small));

    // Rules of your own, checked when they are added (or pass their ledger).
    bw::Engine mine = bw::EngineBuilder()
                          .rules("bitwright 1;\n"
                                 "group my.rules {\n"
                                 "    rule and_or_complement<W>(x: W, y: W) {\n"
                                 "        (x | y) & (x | ~y) => x\n"
                                 "    }\n"
                                 "}")
                          .build();
    assert(mine.simplify((x | y) & (x | ~y)) == x);

    // Errors are exceptions.
    try {
        cx.parse("x +");
        assert(false);
    } catch (const bw::Error &e) {
        assert(e.status() == BW_ERR_SYNTAX);
    }
    return 0;
}

== on expressions compares handles (equal handles, equal expressions); eq, ult, slt and the other comparison methods build 1-bit expressions. >> is the logical shift. An Expr refers to its Context without owning it, so the context must outlive it; Engine is cheap to copy and thread-safe.

Floating point

The three bindings build, print, inspect and evaluate the operations of the chapter on floating point. A floating-point value is a bit-vector holding an IEEE 754 encoding, so operands and results are bit patterns of the format’s width: eb + sb bits for the format (eb, sb), with eb exponent bits and sb significand bits, the hidden bit included. The named formats are F16, BF16, F32, F64, F128, F256 and X87 (in C BW_F32 and so on, and bw_fp_format_of(eb, sb) for any other); the rounding modes are rne, rna, rtp, rtn and rtz, rne by default in Python and C++. Operations have the names of the text syntax: in Python and C++ they are methods (fadd, fsqrt, feq, fisnan, fconvert, to_float, to_int, …); in C, bw_fp builds the node of a bw_fpop, and bw_fp_sub, bw_fp_neg, bw_fp_abs, bw_fp_copysign, bw_fp_cmp, bw_fp_test, bw_x87_load and bw_x87_store build the other operations as the text syntax does, from those nodes and bit-vector operators. A node of an operation has the kind fp (BW_KIND_FP) and its operation as op; its format and rounding mode are format and rounding in Python, fp_node() in C++ and bw_fp_node_of in C.

import struct

import bitwright as bw


def f32(v: float) -> int:
    """The binary32 encoding of `v`."""
    return int(struct.unpack("<I", struct.pack("<f", v))[0])


cx = bw.Context()
a, b = cx.symbols("a b", 32)

# Methods named after the text syntax's operations take the format and a rounding mode.
s = a.fadd(b, bw.F32)
assert str(s) == "fp.add.rne.f32(a, b)"
assert (s.kind, s.op, s.format, s.rounding) == ("fp", "add", bw.F32, "rne")

# 0.1 + 0.2 in binary32: to nearest it rounds up, toward zero one unit in the last place lower.
assert s.eval(a=f32(0.1), b=f32(0.2)) == f32(0.3) == 0x3E99999A
assert a.fadd(b, bw.F32, rm="rtz").eval(a=f32(0.1), b=f32(0.2)) == 0x3E999999

# Integers of any width convert to any format and back, saturating.
i = cx.symbol("i", 16)
x = i.to_float(bw.F64)  # signed; `signed=False` reads `i` as unsigned
assert x.to_int(8, bw.F64, rm="rtz").eval(i=1000) == 0x7F

# Facts see through floating point: a product of converted integers is never a NaN.
nan = x.fmul(x, bw.F64).fisnan(bw.F64)
assert nan.prove() is False
assert str(nan.simplify()) == "0:1"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "bitwright.h"

static void check(bw_status s) {
    if (s != BW_OK) {
        fprintf(stderr, "bitwright error %d: %s\n", s, bw_last_error());
        exit(1);
    }
}

int main(void) {
    bw_context *cx = bw_context_new();
    bw_expr ab[2], s, t, h;
    bw_value v[2], r;
    bw_fp_node n;
    bw_status st;
    char *text;

    /* fp.add.rtz.f32(a, b): the operation, its rounding mode and format, the operands, and the
     * target format and integer width that only conversions read. */
    check(bw_symbol(cx, "a", 32, &ab[0]));
    check(bw_symbol(cx, "b", 32, &ab[1]));
    check(bw_fp(cx, BW_FP_ADD, BW_RTZ, BW_F32, ab, 2, BW_F32, 0, &s));
    check(bw_print(cx, s, 0, &text));
    assert(strcmp(text, "fp.add.rtz.f32(a, b)") == 0);
    bw_string_free(text);

    /* 0.1 + 0.2 in binary32, toward zero. */
    v[0] = bw_value_u64(32, 0x3dcccccd);
    v[1] = bw_value_u64(32, 0x3e4ccccd);
    check(bw_eval(cx, s, ab, v, 2, &r));
    assert(r.limbs[0] == 0x3e999999);

    /* Inspecting the node. */
    check(bw_fp_node_of(cx, s, &n));
    assert(n.op == BW_FP_ADD && n.rm == BW_RTZ && n.format.eb == 8 && n.format.sb == 24);

    /* To a 16-bit signed integer, saturating: 1e10 gives 0x7fff. */
    check(bw_fp(cx, BW_FP_TO_SBV, BW_RTZ, BW_F32, ab, 1, BW_F32, 16, &t));
    v[0] = bw_value_u64(32, 0x501502f9);
    check(bw_eval(cx, t, ab, v, 1, &r));
    assert(r.width == 16 && r.limbs[0] == 0x7fff);

    /* Errors: a format outside 2 <= eb <= 31, sb >= 2, eb + sb <= 512, an operand of another
     * width. */
    st = bw_fp_neg(cx, bw_fp_format_of(1, 9), ab[0], &t);
    assert(st == BW_ERR_WIDTH);
    check(bw_symbol(cx, "h", 16, &h));
    st = bw_fp_test(cx, BW_FP_ISNAN, BW_F32, h, &t);
    assert(st == BW_ERR_WIDTH);

    bw_context_free(cx);
    return 0;
}
#include <cassert>

#include "bitwright.hpp"

namespace bw = bitwright;

int main() {
    bw::Context cx;
    bw::Expr a = cx.symbol("a", 32), b = cx.symbol("b", 32);

    // 0.1 + 0.2 in binary32, to nearest and toward zero.
    bw::Expr near = a.fadd(b, bw::F32), down = a.fadd(b, bw::F32, bw::Rounding::Rtz);
    assert(near.str() == "fp.add.rne.f32(a, b)");
    bw::Value tenth(32, 0x3dcccccd), fifth(32, 0x3e4ccccd);
    assert(cx.eval(near, {{a, tenth}, {b, fifth}}) == bw::Value(32, 0x3e99999a));
    assert(cx.eval(down, {{a, tenth}, {b, fifth}}) == bw::Value(32, 0x3e999999));

    // Inspecting a node, and building it again with the generic `fp`.
    bw::FpNode n = *down.fp_node();
    assert(down.kind() == bw::Kind::Fp && n.op == bw::FpOp::Add && n.format == bw::F32);
    assert(n.rm == bw::Rounding::Rtz && !n.to && !n.int_width);
    assert(bw::fp(n.op, n.format, down.children(), *n.rm) == down);

    // Facts: an integer converted to a float is never a NaN.
    bw::Expr i = cx.symbol("i", 16);
    assert(bw::prove(i.to_float(bw::F64).fisnan(bw::F64)).truth == bw::Truth::False);

    // Errors are exceptions: an operand of another width.
    try {
        a.fadd(i, bw::F32);
        assert(false);
    } catch (const bw::Error &e) {
        assert(e.status() == BW_ERR_WIDTH);
    }
    return 0;
}

Proofs, memory, lifted code and transformations

The same services from each language: an equivalence proved by bitwright’s own SAT solver, synthesis, a spill reloaded through memory, a lifted VEX block, and a transformation refuted.

import bitwright as bw

cx = bw.Context()
x, y = cx.symbols("x y", 32)
assert ((x ^ y) + 2 * (x & y)).equivalent(x + y) is True
cex = (x + y).equivalent(x | y)  # a counterexample: {"x": ..., "y": ...}
assert isinstance(cex, dict)
assert str((((x + y) & 1) ^ (x & 1)).synthesize()) == "y & 1"
z = cx.symbol("z", 32)
assert str((x * y + x * z).saturate()) == "(y + z) * x"

# Memory: a spill and its reload.
sp, v = cx.symbols("sp v", 64)
m = bw.Memory(cx)
m.store(sp - 8, v)
assert m.load(sp - 8, 8) == v

# Lifted code: an MBA as pyvex prints it, back to the sum.
block = bw.lift_vex(
    cx,
    """t0 = GET:I64(rdi)
       t1 = GET:I64(rsi)
       t2 = Xor64(t0,t1)
       t3 = And64(t0,t1)
       t4 = Add64(t3,t3)
       t5 = Add64(t2,t4)
       PUT(rax) = t5""",
)
assert str(block.register("rax").simplify(bw.Engine.deobfuscate())) == "rdi + rsi"

# A transformation, and the precondition it needs.
(r,) = bw.verify_transforms("%r = select %c, %x, false\n=>\n%r = and %c, %x")
assert r.verdict == "invalid" and "poison" in r.text
(i,) = bw.infer_preconditions("%r = mul %x, C\n=>\n%r = shl %x, log2(C)")
assert i.pre == "isPowerOf2(C)"

# The rewrites of a run, and an engine that refuses a pass.
out, steps = bw.Engine().trace(x * 3 - x - x)
assert str(out) == "x" and steps
kept = bw.Engine(refuse=["linear"]).simplify(x * 3 - x - x)
assert kept.equivalent(x) is True
#include <assert.h>
#include <stdio.h>
#include <string.h>

#include "bitwright.h"

int main(void) {
    bw_context *cx = bw_context_new();
    bw_expr a, b, s;
    bw_parse(cx, "(x ^ y) + 2 * (x & y)", 32, &a);
    bw_parse(cx, "x + y", 32, &b);
    int verdict;
    bw_equivalent(cx, a, b, 1000000, &verdict);
    assert(verdict == BW_EQUIVALENT);

    bool found;
    bw_parse(cx, "((x + y) & 1) ^ (x & 1)", 32, &a);
    bw_synthesize(cx, a, 7, &s, &found);
    char *text;
    bw_print(cx, s, 0, &text);
    assert(found && strcmp(text, "y & 1") == 0);
    bw_string_free(text);

    /* Memory: a spill and its reload. */
    bw_memory *m = bw_memory_new("mem", 64, 8, false);
    bw_expr slot, v, back;
    bw_parse(cx, "sp - 8", 64, &slot);
    bw_symbol(cx, "v", 64, &v);
    bw_memory_store(m, cx, slot, v);
    bw_memory_load(m, cx, slot, 8, &back);
    assert(back == v);
    bw_memory_free(m);

    /* Lifted VEX. */
    bw_lifted *l;
    assert(bw_lift(cx, BW_LIFT_VEX, "t0 = GET:I64(rdi)\nt1 = Add64(t0,t0)\nPUT(rax) = t1", NULL,
                   &l) == BW_OK);
    const char *name;
    bw_expr rax;
    bw_lifted_get(l, BW_LIFTED_OUTPUTS, 0, &name, &rax, NULL);
    assert(strcmp(name, "rax") == 0);
    bw_lifted_free(l);

    /* A transformation refuted. */
    char *report;
    bw_transform_counts counts;
    bw_transform_verify("%r = select %c, %x, false\n=>\n%r = and %c, %x", 0, &report, &counts);
    assert(counts.invalid == 1);
    printf("%s", report);
    bw_string_free(report);
    bw_context_free(cx);
    return 0;
}
#include <cassert>

#include "bitwright.hpp"

namespace bw = bitwright;

int main() {
    bw::Context cx;
    bw::Expr e = cx.parse("(x ^ y) + 2 * (x & y)", 32);
    assert(bw::equivalent(e, cx.parse("x + y", 32)) == true);
    assert(bw::synthesize(cx.parse("((x + y) & 1) ^ (x & 1)", 32))->str() == "y & 1");

    bw::Memory m;
    bw::Expr slot = cx.parse("sp - 8", 64), v = cx.parse("v", 64);
    m.store(slot, v);
    assert(m.load(slot, 8).raw() == v.raw());

    auto block = bw::lift(cx, bw::FrontEnd::Vex, "t0 = GET:I64(rdi)\nt1 = Add64(t0,t0)\nPUT(rax) = t1");
    assert(block.reg("rax")->str() == "rdi + rdi");

    auto report = bw::verify_transforms("%r = select %c, %x, false\n=>\n%r = and %c, %x");
    assert(report.invalid == 1);
    return 0;
}

In a compiler

The pieces of In a compiler, with host values as unsigned 64-bit integers of the host’s choosing (an SSA number, a pointer) in place of Rust’s generic value type:

  • The compile strategy. BW_PRESET_COMPILE, Engine::compile(), Engine.compile(). A builder sets its two options on any preset: sharing (BW_SHARING_ROOTS, the default, or BW_SHARING_IGNORED) and the region cap.
  • Declared known bits of a symbol (bw_declare_known), which become part of its meaning, and bw_context_reserve.
  • Host rewrites. A callback that gets a site and a node and returns the node’s replacement, or nothing to leave it. The site views nodes, gives their facts, and builds nodes, charged to the call’s budget.
    • The engine holds a callback to the checks it holds a Rust rewrite to: a result is committed only when it is smaller in the termination order and passes the postconditions, and an untrusted rewrite is compared with the node at sampled points at every application.
    • A callback may run on several threads at once (run_each, an engine shared between threads), so what it reads must be safe for that. In C it must not unwind (the C++ wrapper catches an exception and leaves the node).
    • Python calls it with the interpreter lock held: a callback per node is slow, so Python rewrites are for prototyping. The expressions it gets and builds are views of the site (bw.Site), valid for the call only.
  • Checking a rewrite offline (bw_check_rewrite, check_rewrite): a report of what was covered, or the first failure, with the node, the result and the assignment where they differ.
  • Templates, semantics as text read at run time and shareable between threads.
  • Lowering and raising. A lowering maps host values to expressions; raising a result calls the host’s emit callback for the nodes no host value computes, operands first.
  • The counters of a run (bw_engine_run_stats, the stats argument of run in C++ and Python), host rewrites included.

A function whose pointer parameter is 16-byte aligned, a remainder the compiler’s own range analysis would remove written as a host rewrite, checked and then trusted, and the result raised back into instructions:

import bitwright as bw

cx = bw.Context()
lw = bw.Lowering(cx)
# %0 is a pointer the compiler knows is 16-byte aligned, %1 a byte:
# %2 = and %0, 15; %3 = zext %1; %4 = urem %3, 1000; %5 = add %4, %2
p = lw.input(0, 64, name="p", known=(15, 0))  # (bits known 0, bits known 1)
wide = lw.input(1, 8, name="b").zext(64)
lw.define(2, p & 15)
lw.define(3, wide)
lw.define(4, wide.urem(1000))
v5 = wide.urem(1000) + (p & 15)
lw.define(5, v5)


def needless_rem(site: bw.Site, e: int) -> int | None:
    """`urem(x, c)` as `x` when the facts show `x < c`. Nodes are the site's int handles."""
    if site.op(e) != "urem":
        return None
    x, c = site.children(e)
    bound, facts = site.value(c), site.facts(x)
    return x if bound is not None and facts is not None and facts.umax < bound else None


rem = bw.Rewrite("acme.needless_rem", needless_rem)
assert bw.check_rewrite(rem, ["urem(zext<32>(b), 1000)", "urem(x, 1000)"])
engine = bw.Engine("compile", trusted_rewrites=[rem])
(out,), stats = engine.run([v5], stats=True)
assert out.expr == wide and stats.host.changed == 1

# Raising: %5 is %3 now. An unsigned minimum as a template, raised: two new instructions.
code = []


def emit(node: bw.Expr, operands: list[int]) -> int:
    code.append(f"%{100 + len(code)} = {node.op or node.kind} {operands}")
    return 100 + len(code) - 1


assert lw.raise_(out.expr, emit) == 3 and not code
umin = bw.Template("select(a <u b, a, b)", ["a", "b"])
assert lw.raise_(umin.instantiate([out.expr, p]), emit) == 101
assert code == ["%100 = ult [3, 0]", "%101 = select [100, 3, 0]"]
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>

#include "bitwright.h"

/* `urem(x, c)` as `x` when the facts show `x < c`. */
static bw_expr needless_rem(void *user, bw_site *site, bw_expr e) {
    bw_node n;
    uint64_t c;
    bw_facts f;
    int i;
    (void)user;
    if (!bw_site_node(site, e, &n) || n.kind != BW_KIND_BINARY || n.op != BW_UREM ||
        !bw_site_as_u64(site, n.children[1], &c) || !bw_site_facts(site, n.children[0], &f)) {
        return BW_NULL_EXPR;
    }
    for (i = 1; i < BW_VALUE_LIMBS; i++) {
        if (f.umax.limbs[i] != 0) {
            return BW_NULL_EXPR;
        }
    }
    return f.umax.limbs[0] < c ? n.children[0] : BW_NULL_EXPR;
}

/* Prints an instruction for each new node; new values are numbered from 100. */
static bw_status emit(void *user, const bw_context *cx, bw_expr e, const bw_node *node,
                      const uint64_t *operands, size_t n, uint64_t *value) {
    uint64_t *next = user;
    char *text;
    size_t i;
    (void)node;
    bw_print(cx, e, BW_PRINT_NO_LETS, &text);
    printf("%%%" PRIu64 " = %s   (from", *next, text);
    for (i = 0; i < n; i++) {
        printf(" %%%" PRIu64, operands[i]);
    }
    printf(")\n");
    bw_string_free(text);
    *value = (*next)++;
    return BW_OK;
}

int main(void) {
    bw_context *cx = bw_context_new();
    bw_lowering *lw = bw_lowering_new();
    bw_expr p, b, fifteen, low, wide, thousand, rem, sum;

    /* %0 is a pointer the compiler knows is 16-byte aligned, %1 a byte:
     * %2 = and %0, 15; %3 = zext %1; %4 = urem %3, 1000; %5 = add %4, %2 */
    bw_value aligned = bw_value_u64(64, 15), none = bw_value_u64(64, 0);
    bw_lowering_input(lw, cx, 0, 64, "p", &aligned, &none, &p);
    bw_lowering_input(lw, cx, 1, 8, "b", NULL, NULL, &b);
    bw_const_u64(cx, 64, 15, &fifteen);
    bw_bin(cx, BW_AND, p, fifteen, &low);
    bw_lowering_define(lw, cx, 2, low);
    bw_zext(cx, b, 64, &wide);
    bw_lowering_define(lw, cx, 3, wide);
    bw_const_u64(cx, 64, 1000, &thousand);
    bw_bin(cx, BW_UREM, wide, thousand, &rem);
    bw_lowering_define(lw, cx, 4, rem);
    bw_bin(cx, BW_ADD, rem, low, &sum);
    bw_lowering_define(lw, cx, 5, sum);

    /* The rewrite, checked offline, then linked trusted. */
    bw_rewrite rw = {"acme.needless_rem", NULL, 1, NULL, needless_rem, NULL};
    const char *inputs[] = {"urem(zext<32>(b), 1000)", "urem(x, 1000)"};
    bw_rewrite_report report;
    bw_rewrite_failure *failure;
    assert(bw_check_rewrite(&rw, inputs, 2, NULL, &report, &failure) == BW_OK);
    assert(failure == NULL && report.applications > 0);
    bw_engine_builder *builder = bw_engine_builder_new(BW_PRESET_COMPILE);
    bw_engine_builder_add_rewrite(builder, &rw, true);
    bw_engine *engine;
    assert(bw_engine_builder_build(builder, &engine) == BW_OK);

    /* %5 simplifies to %3: `p & 15` is 0, and the remainder is needless. */
    bw_outcome out;
    bw_stats stats;
    assert(bw_engine_run_stats(engine, cx, &sum, 1, NULL, NULL, &out, &stats) == BW_OK);
    assert(stats.host.changed == 1);
    uint64_t next = 100, v;
    assert(bw_lowering_raise(lw, cx, out.expr, emit, &next, &v) == BW_OK);
    assert(v == 3 && next == 100); /* nothing to emit */

    /* An unsigned minimum as a template, raised: the compare and the select are new. */
    const char *params[] = {"a", "b"};
    bw_template *umin;
    bw_expr args[2] = {out.expr, p}, m;
    assert(bw_template_new("select(a <u b, a, b)", params, 2, &umin) == BW_OK);
    assert(bw_template_instantiate(umin, cx, args, 2, &m) == BW_OK);
    assert(bw_lowering_raise(lw, cx, m, emit, &next, &v) == BW_OK);
    assert(v == 101);

    bw_template_free(umin);
    bw_engine_free(engine);
    bw_engine_builder_free(builder);
    bw_lowering_free(lw);
    bw_context_free(cx);
    return 0;
}

In C++, a rewrite is a bitwright::Rewrite holding a std::function, which the builder copies:

#include <cassert>
#include <string>
#include <vector>

#include "bitwright.hpp"

namespace bw = bitwright;

int main() {
    bw::Context cx;
    bw::Lowering lw(cx);
    bw::Expr p = lw.input(0, "p", 64, bw::KnownBits{bw::Value(64, 15), bw::Value(64, 0)});
    bw::Expr wide = lw.input(1, "b", 8).zext(64);
    lw.define(2, wide);
    bw::Expr v3 = wide.urem(wide.constant(1000)) + (p & 15);
    lw.define(3, v3);

    bw::Rewrite needless_rem{"acme.needless_rem", [](bw::Site &site, bw_expr e) -> bw_expr {
        auto n = site.node(e);
        if (!n || n->kind != bw::Kind::Binary || n->op != BW_UREM) {
            return BW_NULL_EXPR;
        }
        auto c = site.as_u64(n->children[1]);
        auto f = site.facts(n->children[0]);
        auto hi = f ? f->umax.to_u64() : std::nullopt;
        return c && hi && *hi < *c ? n->children[0] : BW_NULL_EXPR;
    }};
    assert(bw::check_rewrite(needless_rem, {"urem(zext<32>(b), 1000)", "urem(x, 1000)"}));

    bw::Engine engine = bw::EngineBuilder(bw::Preset::Compile).rewrite(needless_rem, true).build();
    bw::Stats stats;
    auto out = engine.run({v3}, nullptr, nullptr, &stats);
    assert(out[0].expr == wide && stats.host.changed == 1);

    bw::Template umin("select(a <u b, a, b)", {"a", "b"});
    bw::Expr m = umin.instantiate(cx, {out[0].expr, p});
    std::vector<std::string> code;
    uint64_t v = lw.raise(m, [&](bw::Expr e, const bw::Node &, const std::vector<uint64_t> &ops) {
        code.push_back(e.str() + " from " + std::to_string(ops.size()) + " operands");
        return uint64_t(100 + code.size());
    });
    assert(v == 102 && code.size() == 2 && lw.owner(m) == uint64_t(102));

    // A wrong rewrite, caught offline with a counterexample.
    bw::Rewrite wrong{"acme.wrong", [](bw::Site &site, bw_expr e) -> bw_expr {
        auto n = site.node(e);
        return n && n->kind == bw::Kind::Binary && n->op == BW_OR ? n->children[0] : BW_NULL_EXPR;
    }};
    bw::RewriteReport report = bw::check_rewrite(wrong, {"x | y"});
    assert(!report && report.failure->kind == bw::RewriteFailureKind::Differs);
    assert(!report.failure->assignment.empty());
    return 0;
}

JavaScript (WebAssembly)

bitwright-wasm/ builds bitwright as a WebAssembly module with no bindings generator, and bitwright-wasm/js/bitwright.mjs wraps it for JavaScript, in a browser or Node: every function takes and returns strings.

cargo build --release -p bitwright-wasm --target wasm32-unknown-unknown
# target/wasm32-unknown-unknown/release/bitwright_wasm.wasm
import { readFile } from "node:fs/promises";
import { instantiate } from "./bitwright.mjs";

const bw = await instantiate(await readFile("bitwright_wasm.wasm")); // or fetch(url)
bw.simplify("(x & y) * (x | y) + (x & ~y) * (~x & y)", 64); // "x * y"
bw.synthesize("((x + y) & 1) ^ (x & 1)", 32);               // "y & 1"
bw.equivalent("x * 2", "x + x", 16);                         // "equivalent"
bw.prove("%r = select %c, %x, false\n=>\n%r = and %c, %x");   // "INVALID …" and why
bw.lift("vex", "t0 = GET:I64(rdi)\nt1 = Add64(t0,t0)\nPUT(rax) = t1"); // "rax = rdi + rdi\n"

validate (translation validation), infer (preconditions) and toSmtlib complete it; bitwright-wasm/js/test.mjs exercises each under Node.

For a compiler, simplify takes more options: engine: "compile", a symbol’s declared known bits (known), host rewrites as JavaScript functions (rewrites), sharing, maxRegion, maxRounds, and stats (the result is then { expr, stats }). A rewrite sees nodes as BigInt handles through its site, as in Python; an exception it throws is rethrown when the call ends. checkRewrite tests one offline, and template and checkTemplate read semantics as text. The module keeps no context between calls, so there is no lowering: a JavaScript host lowers by writing its values as expression text.

const needlessRem = {
  name: "acme.needless_rem",
  // `urem(x, c)` as `x` when the facts show `x < c`.
  rewrite: (site, e) => {
    if (site.op(e) !== "urem") return null;
    const [x, c] = site.children(e);
    const bound = site.value(c), facts = site.facts(x);
    return bound !== null && facts !== null && facts.umax < bound ? x : null;
  },
};
bw.checkRewrite(needlessRem, ["urem(zext<32>(b), 1000)"]).passed; // true
bw.simplify("urem(zext<64>(b:8), 1000) + (p & 15)", 64, {
  engine: "compile",
  known: { p: [15n, 0n] }, // 16-byte aligned: bits known 0, bits known 1
  rewrites: [{ ...needlessRem, trusted: true }],
}); // "zext<64>(b)"
bw.template("select(a <u b, a, b)", ["a", "b"], ["x", "y"], 32); // "select(x <u y, x, y)"

Examples

Worked examples, one task each, in the order a reverse engineer tends to meet them: code an obfuscator made unreadable, conditions a lifter spelled out from flags, values a compiler shuffled, floating point, and what to do with the results. Every one is a complete program that runs as a test; the chapters they point to explain the machinery.

Most examples use one of two engines:

  • Engine::standard(): the built-in rules and the normal-form passes. Fast, and enough for anything that is not mixed boolean-arithmetic.
  • The deobfuscation engine, what the command line’s simplify runs: the deobfuscation strategy and the MBA service with bitwright’s own normal-form solver, taking only answers bitwright can prove itself (see Deobfuscation and MBA).

Undoing mixed boolean-arithmetic

Obfuscators replace an operation with an identity that mixes arithmetic and bitwise logic. The deobfuscation engine reads such expressions as functions of their bits and brings them back, linear ones, ones with constants inside the bitwise parts, and products:

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Strategy};
use bitwright::mba::MbaConfig;
use bitwright::{Context, ParseOptions, Width};

// The command line's `simplify`: the MBA service with its defaults, the normal-form solver
// and answers published only on bitwright's own proof.
let engine = Engine::builder()
    .builtin()
    .strategy(Strategy::deobfuscate().with_mba(MbaConfig::default()))
    .build()?;
let mut cx = Context::new();
let o = ParseOptions::width(Width::W64);
for (obfuscated, plain) in [
    ("(x ^ y) + 2 * (x & y)", "x + y"),
    ("(x + y) - 2 * (x & y)", "x ^ y"),
    ("(x & ~y) - (~x & y)", "x - y"),
    ("(x ^ 0x10) + 2 * (x & 0x10)", "x + 16"),
    ("3 * (x & 0x55) + 3 * (x & 0xaa)", "(x & 255) * 3"),
    ("(x & y) * (x | y) + (x & ~y) * (~x & y)", "x * y"),
] {
    let e = cx.parse(obfuscated, &o)?;
    let out = engine.simplify(&mut cx, e)?;
    assert_eq!(cx.display(out.expr).to_string(), plain, "{obfuscated}");
}
Ok::<(), Box<dyn std::error::Error>>(())
}

Encoded constants and arithmetic

Constants are hidden behind multiplications by an odd number and its inverse, complements that cancel, and terms that add up to nothing. The standard engine folds them; no MBA solver is needed:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let engine = Engine::standard();
let simplify = |src: &str, w: Width| -> Result<String, bitwright::Error> {
    let mut cx = Context::new(); // a symbol has one width in a context
    let e = cx.parse(src, &ParseOptions::width(w))?;
    let out = engine.simplify(&mut cx, e)?;
    Ok(cx.display(out.expr).to_string())
};
// 7 · 183 = 1281 = 1 (mod 256): multiplying by 7 and then by 183 is multiplying by 1.
assert_eq!(simplify("x * 7 * 183", Width::W8)?, "x");
// `a ^ 0xffff` is `~a` at 16 bits, and `~a` is `−a − 1`: the constants cancel.
assert_eq!(simplify("((x + 0x1234) ^ 0xffff) + 0x1235 + (x ^ 0xffff) + 1", Width::W16)?, "x * -2");
// A term, twice, minus twice the term.
assert_eq!(simplify("(x ^ 0x5a5a5a5a) + (x ^ 0x5a5a5a5a) - 2 * (x ^ 0x5a5a5a5a)", Width::W32)?, "0:32");
Ok::<(), bitwright::Error>(())
}

Opaque predicates and dead code

An opaque predicate is a condition the obfuscator knows the value of, which guards a branch that never runs. When the value follows from which bits of the operands are known, or from their ranges, facts decide it and the simplifier folds it; Context::prove answers the question without rewriting anything:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{CmpOpExt, Context, ParseOptions, Query, Truth, Width};

let engine = Engine::standard();
let mut cx = Context::new();
let o = ParseOptions::width(Width::W8);
for (predicate, value) in [
    ("((x | 1) & 1) == 1", "1:1"), // an odd number is odd
    ("(x | 0x80) >u 0x7f", "1:1"), // with the top bit set, above 127
    ("(x & 0x0f) + 1 == 0", "0:1"), // at most 16
    ("((x * 4) & 3) != 0", "0:1"), // a multiple of 4 has no low bits
] {
    let e = cx.parse(predicate, &o)?;
    let out = engine.simplify(&mut cx, e)?;
    assert_eq!(cx.display(out.expr).to_string(), value, "{predicate}");
}
// Asked directly: the bits a shift pushed out are gone.
let dead = cx.parse("(x & 0x0f) >>u 4", &o)?;
assert_eq!(cx.prove(Query::IsZero(dead))?, Truth::True);
let low = cx.parse("x & 0x0f", &o)?;
let sixteen = cx.parse("16", &o)?;
assert_eq!(cx.prove(Query::Cmp(CmpOpExt::Ult, low, sixteen))?, Truth::True);
Ok::<(), bitwright::Error>(())
}

Conditions from lifted flags

A lifter turns a compare-and-branch into the flags the instruction sets and the condition the branch tests. For a signed jl after cmp a, b that is SF != OF, with the sign flag and the overflow flag of the subtraction written out bit by bit; the simplifier brings back the comparison. Range checks that a compiler emitted for a switch combine into one:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let engine = Engine::standard();
let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
for (lifted, condition) in [
    // SF ^ OF, both as bit 31 of an expression.
    ("((a - b) >>u 31) ^ (((a ^ b) & (a ^ (a - b))) >>u 31)", "zext<32>(a <s b)"),
    // The same with the text syntax's overflow operator.
    ("(a - b <s 0) != ssub_overflow(a, b)", "a <s b"),
    // `case 4 ... 9:` and `case 10:` of a switch.
    ("(x - 4 <=u 5) | (x == 10)", "x - 4 <=u 6"),
] {
    let e = cx.parse(lifted, &o)?;
    let out = engine.simplify(&mut cx, e)?;
    assert_eq!(cx.display(out.expr).to_string(), condition, "{lifted}");
}
Ok::<(), bitwright::Error>(())
}

Floating-point comparisons set the flags too: after ucomiss x, y, ZF is “equal or unordered”, PF “unordered” and CF “less or unordered”, and ja tests !CF & !ZF. The comparisons combine as floats, NaNs included:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let unordered = "(fp.isnan.f32(x) | fp.isnan.f32(y))";
let cf = format!("(fp.lt.f32(x, y) | {unordered})");
let zf = format!("(fp.eq.f32(x, y) | {unordered})");
let e = cx.parse(&format!("~{cf} & ~{zf}"), &o)?; // ja
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "fp.lt.f32(y, x)");
let e = cx.parse(&format!("{cf} | {zf}"), &o)?; // jbe
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "~fp.lt.f32(y, x)");
Ok::<(), bitwright::Error>(())
}

Shuffled bits

Values taken apart and put back, and shifts that only clear bits, come back as the one operation they are:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let engine = Engine::standard();
let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
for (shuffled, plain) in [
    ("(x << 8) | (x >>u 24)", "rotl(x, 8)"),
    ("(x >>u 3) << 3", "x & -8"),
    ("trunc<8>(zext<32>(b:8) + 256)", "b"),
] {
    let e = cx.parse(shuffled, &o)?;
    let out = engine.simplify(&mut cx, e)?;
    assert_eq!(cx.display(out.expr).to_string(), plain, "{shuffled}");
}
Ok::<(), bitwright::Error>(())
}

The deobfuscation strategy traces bits through extracts, shifts and masks as well, and recognizes byte swaps and extensions (see Deobfuscation and MBA).

Hash comparisons

A license check compares a keyed hash of the input with a constant. Each step of the hash is invertible, so the comparison is one of the input with the constant’s preimage (see Invertibility):

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let mut cx = Context::new();
let e = cx.parse("((x ^ 0x5a) * 0x1d) == 0x33", &ParseOptions::width(Width::W8))?;
let out = Engine::standard().simplify(&mut cx, e)?;
// (0xd5 ^ 0x5a) · 0x1d = 0x33 (mod 256), and no other input hashes to 0x33.
assert_eq!(cx.display(out.expr).to_string(), "x == -43");
Ok::<(), bitwright::Error>(())
}

Floating point

Floats are bit-vectors holding their encodings (see Floating point). Construction applies what holds bit for bit, facts read encodings as sets of floats, and the built-in floating-point rules apply identities on numbers where the facts show an operand is no NaN:

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width};

let engine = Engine::standard();
let mut cx = Context::new();
let o = ParseOptions::width(Width::W64);
for (float, plain) in [
    // An integer converted to a float is never a NaN.
    ("fp.isnan.f64(fp.from_sbv.rne.f64(i:32))", "0:1"),
    // ... so multiplying it by 1.0 changes nothing.
    ("fp.mul.rne.f64(fp.from_sbv.rne.f64(i:32), 0x3ff0000000000000)", "fp.from_sbv.rne.f64(i)"),
    // Dividing by 4.0 is multiplying by 0.25, exactly.
    ("fp.div.rne.f64(x, 0x4010000000000000)", "fp.mul.rne.f64(x, 0x3fd0000000000000)"),
    // A 16-bit integer survives a round trip through binary64.
    ("fp.to_sbv.rtz.f64<32>(fp.from_sbv.rne.f64(j:16))", "sext<32>(j)"),
    // Every encoding is in one of the five classes.
    ("fp.isnan.f64(x) | fp.isinf.f64(x) | fp.iszero.f64(x) | fp.isnormal.f64(x) \
      | fp.issubnormal.f64(x)", "1:1"),
] {
    let e = cx.parse(float, &o)?;
    let out = engine.simplify(&mut cx, e)?;
    assert_eq!(cx.display(out.expr).to_string(), plain, "{float}");
}
Ok::<(), bitwright::Error>(())
}

x · 1.0 of an unknown x stays: for a NaN with a payload the product is the canonical NaN, a different bit pattern.

Under path constraints

On a path through a function, the branches taken so far constrain the values. Assumptions carries them, and each result says which of them it relied on, so it is used only where they hold (see Constraints):

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Run};
use bitwright::{Assumptions, Context, ParseOptions, Width};

let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let mut path = Assumptions::new();
let bounded = cx.parse("x <u 16", &o)?; // after `cmp x, 16; jae skip`, not taken
let three = cx.parse("y == 3", &o)?;
let b = path.assume_true(&mut cx, bounded)?;
let t = path.assume_true(&mut cx, three)?;

let e = cx.parse("(x & 0xf0) + y * x", &o)?;
let out = Engine::standard().run(&mut cx, &[e], Run::default().with_assumptions(&path))?;
let r = out.roots[0];
assert_eq!(cx.display(r.expr).to_string(), "x * 3");
assert!(r.relies_on.may_use(b) && r.relies_on.may_use(t));
// Without them, nothing is known.
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x * y + (x & 240)");
Ok::<(), bitwright::Error>(())
}

A rule of your own

When a target has an idiom of its own, write it as a rule, check it, and link it with the ledger the check produces (see Writing rules and Checking rules). A guard can ask the facts: this rule drops a shift pair whose low bits are known to be zero:

#![allow(unused)]
fn main() {
use bitwright::check::{CheckConfig, check_program};
use bitwright::engine::Engine;
use bitwright::rules::{Ledger, RuleProgram};
use bitwright::{Context, ParseOptions, Width};

let src = "bitwright 1;
group my.idioms {
    /// Shifting right and back left clears the low bits; when they are zero already, it
    /// changes nothing.
    #[example(\"((p & 0xf0) >>u 4) << 4\" => \"p & 0xf0\")]
    rule shift_pair_known_zero<W>(x: W, c: const W) {
        (x >>u c) << c => x
        if zero_bits(x, m)
        let m: W = (one << c) - 1
    }
}";
let program = RuleProgram::compile(src).map_err(|e| e.render("idioms.bwr", src))?;
let checks = check_program(&program, &CheckConfig::default());
assert!(checks.iter().all(|c| c.is_sound() && c.examples.is_empty()));
let ledger = Ledger::from_checks(&checks);

// Only this rule, to show it at work.
let engine = Engine::builder().program(program, &ledger).build().map_err(|e| e.to_string())?;
let mut cx = Context::new();
let e = cx.parse("((r & 0xffff0000) >>u 16) << 16", &ParseOptions::width(Width::W32))
    .map_err(|e| e.to_string())?;
let out = engine.simplify(&mut cx, e).map_err(|e| e.to_string())?;
assert_eq!(cx.display(out.expr).to_string(), "r & -65536");
Ok::<(), String>(())
}

Many expressions at once

An analysis simplifies many expressions that share subterms. Engine::run takes them together; work on a shared subterm is done once, and results are remembered in the context, so asking again costs nothing:

#![allow(unused)]
fn main() {
use bitwright::engine::{Engine, Run};
use bitwright::{Context, ParseOptions, Width};

let engine = Engine::standard();
let mut cx = Context::new();
let o = ParseOptions::width(Width::W32);
let roots = [
    cx.parse("((a ^ b) + 2 * (a & b)) & 0xff", &o)?,
    cx.parse("((a ^ b) + 2 * (a & b)) >>u 24", &o)?,
    cx.parse("(a ^ b) + 2 * (a & b) == 0", &o)?,
];
let out = engine.run(&mut cx, &roots, Run::default())?;
let shown: Vec<String> = out.roots.iter().map(|r| cx.display(r.expr).to_string()).collect();
assert_eq!(shown, ["a + b & 255", "a + b >>u 24", "a + b == 0"]);
// The same roots again: answered from the context's memory.
let again = engine.run(&mut cx, &roots, Run::default())?;
assert!(again.stats.memo_hits > 0);
Ok::<(), bitwright::Error>(())
}

Handing results to a solver

A result can be checked independently: smtlib::equivalence_query writes an SMT-LIB script that asserts the input and the result differ, which any solver answers unsat if they are equal. And a script from another tool reads back into a context (see SMT-LIB):

#![allow(unused)]
fn main() {
use bitwright::engine::Engine;
use bitwright::{Context, ParseOptions, Width, smtlib};

let mut cx = Context::new();
let e = cx.parse("(x | y) - (x & y)", &ParseOptions::width(Width::W32))?;
let out = Engine::standard().simplify(&mut cx, e)?;
assert_eq!(cx.display(out.expr).to_string(), "x ^ y");
let query = smtlib::equivalence_query(&mut cx, e, out.expr)?;
assert!(query.contains("(check-sat)")); // `z3 -in` or `bitwuzla` answers `unsat`

// A term from another tool, simplified.
let script = "(declare-const a (_ BitVec 16))
(declare-const b (_ BitVec 16))
(define-fun t () (_ BitVec 16) (bvadd (bvxor a b) (bvmul #x0002 (bvand a b))))";
let t = smtlib::import(&mut cx, script)?.definition("t").expect("t");
let out = Engine::standard().simplify(&mut cx, t)?;
assert_eq!(cx.display(out.expr).to_string(), "a + b");
Ok::<(), bitwright::Error>(())
}

The same operations are in the C, C++ and Python bindings; Examples in Python, C and C++ has examples in each.

Examples in Python, C and C++

The tasks of the Rust examples, from the bindings: feeding a disassembler’s or a lifter’s expressions to bitwright and taking the results back. Every example is a complete program, run as a test.

Python

A batch of obfuscated expressions

bw.Engine.deobfuscate() is the command line’s simplify: the deobfuscation strategy and the MBA service, publishing only what bitwright proves. Text that a script collected, say from a decompiler’s output, parses at the width of its variables:

import bitwright as bw

cx = bw.Context()
engine = bw.Engine.deobfuscate()
collected = {
    "(x ^ y) + 2 * (x & y)": "x + y",
    "(x & ~y) - (~x & y)": "x - y",
    "(x ^ 0x10) + 2 * (x & 0x10)": "x + 16",
    "(x & y) * (x | y) + (x & ~y) * (~x & y)": "x * y",
    "(x ^ 0x5a5a5a5a) + (x ^ 0x5a5a5a5a) - 2 * (x ^ 0x5a5a5a5a)": "0:32",
}
for text, plain in collected.items():
    assert str(engine.simplify(cx.parse(text, 32))) == plain, text

# One expression, without a context: the command line's `simplify` exactly.
assert bw.simplify("x * 7 * 183", 8) == "x"
assert bw.simplify("x & 0xf0", 32, assume=["x <u 16"]) == "0:32"

From a lifter’s instructions

A lifter hands over instructions, not text. Operators and methods build the expressions they compute; here a signed jl after cmp eax, ebx, with the flags written out as the instruction defines them:

import bitwright as bw

cx = bw.Context()
regs = {"eax": cx.symbol("a", 32), "ebx": cx.symbol("b", 32)}

# cmp eax, ebx; jl target
a, b = regs["eax"], regs["ebx"]
diff = a - b
sf = diff.bit(31)                   # the sign of the difference
of = ((a ^ b) & (a ^ diff)).bit(31)  # the operands' signs differ, and the result's is b's
jl = sf ^ of

assert str(jl.simplify()) == "a <s b"
# The other signed conditions follow from it.
assert str((~jl).simplify()) == "b <=s a"  # jge

Opaque predicates

Walking a function’s branches, each condition is either decided, so the other arm is dead code, or not. prove() answers True, False or None:

import bitwright as bw

cx = bw.Context()
x = cx.symbol("x", 32)
branches = {
    "odd": ((x | 1) & 1).eq(1),        # always taken
    "aligned": ((x << 2) & 3).ne(0),   # never: shifted by 2, the low bits are 0
    "overflow": (x & 0xFF).ugt(0xFF),  # never
    "input": x.ugt(100),               # depends on the input
}
decided = {name: cond.prove() for name, cond in branches.items()}
assert decided == {"odd": True, "aligned": False, "overflow": False, "input": None}

Under a path condition

The branches taken to reach a block are assumptions; each result says which of them it relied on, so it is kept only where they hold:

import bitwright as bw

cx = bw.Context()
x, y = cx.symbols("x y", 32)
path = bw.Assumptions()
bounded = path.assume(x.ult(16))  # constraint 0
three = path.assume(y.eq(3))      # constraint 1

out = bw.Engine().run([(x & 0xF0) + y * x, x + 1], assumptions=path)
assert str(out[0].expr) == "x * 3" and out[0].relies_on == (bounded, three)
assert str(out[1].expr) == "x + 1" and out[1].relies_on == ()

# A branch not taken assumes the negation.
path.assume(x.eq(0), holds=False)
assert x.ugt(0).prove(path) is True

A keyed check

A serial check compares a keyed hash with a constant. Each step is invertible, so the check has exactly one solution, which the simplifier finds:

import bitwright as bw

cx = bw.Context()
k = cx.symbol("k", 8)
h = (k ^ 0x5A) * 0x1D
assert h.prove_injective(k, bijective=True) is True
check = h.eq(0x33)
assert str(check.simplify()) == "k == -43"
assert h.eval(k=0xD5) == 0x33  # -43 is 0xd5

Floating-point flags

After ucomiss xmm0, xmm1, ja tests that neither the carry flag (“less, or unordered”) nor the zero flag (“equal, or unordered”) is set. Built from the flags, the condition comes back as one comparison:

import bitwright as bw

cx = bw.Context()
x, y = cx.symbols("x y", 32)
f = bw.F32
unordered = x.fisnan(f) | y.fisnan(f)
cf = x.flt(y, f) | unordered
zf = x.feq(y, f) | unordered
ja = ~cf & ~zf
assert str(ja.simplify()) == "fp.lt.f32(y, x)"

# Integers that went through a float and back.
i = cx.symbol("i", 16)
round_trip = i.to_float(bw.F64).to_int(32, bw.F64, rm="rtz")
assert str(round_trip.simplify()) == "sext<32>(i)"

To a solver and back

to_smtlib writes the expressions as an SMT-LIB script for any solver; from_smtlib reads one back, symbols, definitions and assertions:

import bitwright as bw

cx = bw.Context()
x, y = cx.symbols("x y", 32)
before = (x | y) - (x & y)
after = before.simplify()
assert str(after) == "x ^ y"
script = cx.to_smtlib(before, after)
assert "(define-fun root0 " in script and "(define-fun root1 " in script

other = bw.Context()
read = other.from_smtlib(script)
root0, root1 = read.definitions["root0"], read.definitions["root1"]
assert root0.simplify() == root1  # one node: `x ^ y` in the other context

C

Deobfuscating under assumptions

The deobfuscation preset, several roots in one call, and the constraints each result relies on as a mask:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "bitwright.h"

static void check(bw_status s) {
    if (s != BW_OK) {
        fprintf(stderr, "bitwright error %d: %s\n", s, bw_last_error());
        exit(1);
    }
}

static void expect(bw_context *cx, bw_expr e, const char *want) {
    char *text;
    check(bw_print(cx, e, 0, &text));
    if (strcmp(text, want) != 0) {
        fprintf(stderr, "got %s, want %s\n", text, want);
        exit(1);
    }
    bw_string_free(text);
}

int main(void) {
    bw_context *cx = bw_context_new();
    bw_engine *engine = bw_engine_new(BW_PRESET_DEOBFUSCATE);
    bw_assumptions *path = bw_assumptions_new();
    bw_expr roots[2], cond;
    bw_outcome out[2];
    uint32_t id;

    check(bw_parse(cx, "(x & y) * (x | y) + (x & ~y) * (~x & y)", 64, &roots[0]));
    check(bw_parse(cx, "(x & 0xf0) + (x ^ y) + 2 * (x & y)", 64, &roots[1]));

    /* On this path, x is below 16. */
    check(bw_parse(cx, "x <u 16", 64, &cond));
    check(bw_assume(path, cx, cond, true, &id));
    assert(id == 0);

    check(bw_engine_run(engine, cx, roots, 2, NULL, path, out));
    expect(cx, out[0].expr, "x * y");
    assert(out[0].end == BW_END_COMPLETED && out[0].relies_on == 0);
    expect(cx, out[1].expr, "x + y");
    assert(out[1].relies_on == 1); /* bit 0: constraint 0 */

    bw_assumptions_free(path);
    bw_engine_free(engine);
    bw_context_free(cx);
    return 0;
}

Deciding branches

bw_prove and bw_prove_cmp answer BW_TRUE, BW_FALSE or BW_UNKNOWN; bw_facts_of says what is known about a value:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

#include "bitwright.h"

static void check(bw_status s) {
    if (s != BW_OK) {
        fprintf(stderr, "bitwright error %d: %s\n", s, bw_last_error());
        exit(1);
    }
}

int main(void) {
    bw_context *cx = bw_context_new();
    bw_expr odd, low, limit, input;
    bw_proof p;
    bw_facts f;

    check(bw_parse(cx, "((x | 1) & 1) == 1", 32, &odd));
    check(bw_prove(cx, odd, NULL, &p));
    assert(p.truth == BW_TRUE);

    check(bw_parse(cx, "x & 0xff", 32, &low));
    check(bw_const_u64(cx, 32, 0x100, &limit));
    check(bw_prove_cmp(cx, BW_ULT, low, limit, NULL, &p));
    assert(p.truth == BW_TRUE);

    check(bw_parse(cx, "x >u 100", 32, &input));
    check(bw_prove(cx, input, NULL, &p));
    assert(p.truth == BW_UNKNOWN);

    /* The low byte, shifted up by 4, then with bit 0 set. */
    check(bw_parse(cx, "(x & 0xff) << 4 | 1", 32, &low));
    check(bw_facts_of(cx, low, NULL, &f));
    assert(f.known_one.limbs[0] == 1 && f.known_zero.limbs[0] == 0xfffff00e);
    assert(f.umin.limbs[0] == 1 && f.umax.limbs[0] == 0xff1 && f.ustride == 16);

    bw_context_free(cx);
    return 0;
}

An SMT-LIB script in, a simplified one out

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "bitwright.h"

static void check(bw_status s) {
    if (s != BW_OK) {
        fprintf(stderr, "bitwright error %d: %s\n", s, bw_last_error());
        exit(1);
    }
}

int main(void) {
    const char *script = "(declare-const a (_ BitVec 32))\n"
                         "(declare-const b (_ BitVec 32))\n"
                         "(define-fun t () (_ BitVec 32) (bvadd (bvxor a b) (bvmul #x00000002 "
                         "(bvand a b))))\n";
    bw_context *cx = bw_context_new();
    bw_engine *engine = bw_engine_new(BW_PRESET_DEOBFUSCATE);
    bw_smt_import *imp;
    bw_expr t, s;
    const char *name;
    char *out;

    check(bw_smtlib_import(cx, script, &imp));
    assert(bw_smt_import_count(imp, BW_SMT_SYMBOLS) == 2);
    assert(bw_smt_import_count(imp, BW_SMT_DEFINITIONS) == 1);
    check(bw_smt_import_get(imp, BW_SMT_DEFINITIONS, 0, &t, &name));
    assert(strcmp(name, "t") == 0);
    bw_smt_import_free(imp);

    check(bw_simplify(engine, cx, t, &s));
    check(bw_smtlib_export(cx, &s, 1, &out));
    /* root0 is (bvadd |a| |b|): the MBA is gone. */
    assert(strstr(out, "(define-fun root0 () (_ BitVec 32)") != NULL);
    assert(strstr(out, "bvadd") != NULL && strstr(out, "bvand") == NULL);
    bw_string_free(out);

    bw_engine_free(engine);
    bw_context_free(cx);
    return 0;
}

C++

Flags from a lifter

The jl of the Python example, and the carry flag of an unsigned compare, built with the C++ wrapper’s operators:

#include <cassert>

#include "bitwright.hpp"

namespace bw = bitwright;

int main() {
    bw::Context cx;
    bw::Expr a = cx.symbol("a", 32), b = cx.symbol("b", 32);
    bw::Engine engine = bw::Engine::standard();

    // cmp a, b
    bw::Expr diff = a - b;
    bw::Expr sf = diff.extract(31, 1);
    bw::Expr of = ((a ^ b) & (a ^ diff)).extract(31, 1);
    bw::Expr cf = a.ult(b);
    bw::Expr zf = diff.eq(diff.constant(0));

    assert(engine.simplify(sf ^ of).str() == "a <s b");  // jl
    assert(engine.simplify(cf | zf).str() == "a <=u b"); // jbe
    assert(engine.simplify(~cf & ~zf).str() == "b <u a"); // ja
    return 0;
}

Proofs, invertibility and budgets

#include <cassert>

#include "bitwright.hpp"

namespace bw = bitwright;

int main() {
    bw::Context cx;
    bw::Expr k = cx.symbol("k", 8);

    // A keyed mixer is a bijection of its key, so a check against a constant has one solution.
    bw::Expr h = (k ^ 0x5a) * 0x1d;
    assert(bw::prove_injective(h, k, true));
    assert(bw::Engine::standard().simplify(h.eq(k.constant(0x33))).str() == "k == -43");

    // Opaque predicates, under a path condition.
    bw::Assumptions path;
    uint32_t small = path.assume(k.ult(k.constant(16)));
    bw::Proof p = bw::prove((k & 0xf0).eq(k.constant(0)), &path);
    assert(p.truth == bw::Truth::True && p.relies_on == (uint64_t{1} << small));
    assert(bw::prove(k.ugt(k.constant(100))).truth == bw::Truth::Unknown);

    // A budget bounds the work; a stopped result is correct, just not final.
    bw::Expr mba = cx.parse("(x & y) * (x | y) + (x & ~y) * (~x & y)", 64);
    bw::Budget none;
    none.node_visits = 0;
    bw::Engine deob = bw::Engine::deobfuscate();
    assert(deob.run({mba}, &none)[0].end == bw::End::Budget);
    std::vector<bw::Outcome> out = deob.run({mba});
    assert(out[0].end == bw::End::Completed && out[0].expr.str() == "x * y");
    return 0;
}

Floating point

#include <cassert>

#include "bitwright.hpp"

namespace bw = bitwright;

int main() {
    bw::Context cx;
    bw::Expr x = cx.symbol("x", 32), y = cx.symbol("y", 32);
    bw::Engine engine = bw::Engine::standard();

    // ucomiss x, y; ja
    bw::Expr unordered = x.fisnan(bw::F32) | y.fisnan(bw::F32);
    bw::Expr cf = x.flt(y, bw::F32) | unordered, zf = x.feq(y, bw::F32) | unordered;
    assert(engine.simplify(~cf & ~zf).str() == "fp.lt.f32(y, x)");

    // An integer converted to a double: never a NaN, so multiplying it by 1.0 is a no-op.
    bw::Expr i = cx.symbol("i", 32);
    bw::Expr d = i.to_float(bw::F64);
    bw::Expr one = d.constant(0x3ff0000000000000);
    assert(engine.simplify(d.fmul(one, bw::F64)) == d);
    // Dividing by 4.0 is multiplying by 0.25, exactly.
    bw::Expr four = d.constant(0x4010000000000000);
    assert(engine.simplify(d.fdiv(four, bw::F64)).str() ==
           "fp.mul.rne.f64(fp.from_sbv.rne.f64(i), 0x3fd0000000000000)");
    return 0;
}

Stability

bitwright stays at 0.x until its API has been used in production for a release cycle. Within that, the rules below already hold.

API. Every public enum (other than closed sets such as Truth) and every configuration struct is #[non_exhaustive], so adding a variant or a field is not a breaking change. Configuration structs have Default (or a constructor such as Deadline::new) and a with_* setter per field; build them that way:

#![allow(unused)]
fn main() {
use bitwright::engine::{Budget, Run};

let run = Run::default().with_per_call(Budget::default().with_rewrites(1_000));
let _ = run;
}

cargo-semver-checks runs against the last release in continuous integration. The minimum supported Rust version is 1.88; raising it is a minor-version change.

Results. Users snapshot printed output, so simplification results have their own contract: a patch release never changes a result except to fix unsoundness (named in the changelog by rule id); a minor release may, and lists the changes under “Behavior changes”. The printer’s format and the canonical order change only in minor releases.

Rule files. The rule-language edition (bitwright 1;) is frozen within a major version, and diagnostic codes are stable: a code is never reused for a different meaning.

Determinism. The same input gives the same output on every run and every platform: no global state, no hash-map iteration order in results, no environment variables, and no clock unless you supply a deadline (which then decides only when to stop).

Rule catalog

The built-in rules: core.bwr, the local rules the engine links by default, and eqsat.bwr, the equations of the equality-saturation search. Every rule is checked (and recorded in the file’s proof ledger) and every example holds.

core.bwr

core.bitwise

and_absorb_or — rule

Absorption.

rule and_absorb_or<W>(x: W, y: W) { x & (x | y) => x }
  • (q | p) & p → p

or_absorb_and — rule

Absorption.

rule or_absorb_and<W>(x: W, y: W) { x | (x & y) => x }
  • (q & p) | p → p

and_not_self — rule

Complement.

rule and_not_self<W>(x: W) { x & ~x => 0 }
  • p & ~p → 0:8

or_not_self — rule

Complement.

rule or_not_self<W>(x: W) { x | ~x => ones }
  • p | ~p → 255:8

xor_not_self — rule

Complement.

rule xor_not_self<W>(x: W) { x ^ ~x => ones }
  • p ^ ~p → 255:8

xor_and_or — rule

The and and the or of two values differ exactly in their xor.

rule xor_and_or<W>(x: W, y: W) { (x & y) ^ (x | y) => x ^ y }
  • (p & q) ^ (p | q) → p ^ q

and_mask_redundant — rule

A mask that keeps every bit the value may have set.

rule and_mask_redundant<W>(x: W, c: const W) { x & c => x if zero_bits(x, ~c) }
  • (q & 15) & 0x3f → q & 15

and_mask_disjoint — rule

A mask that keeps none of the bits the value may have set.

rule and_mask_disjoint<W>(x: W, c: const W) { x & c => 0 if zero_bits(x, c) }
  • (q & 0xf0) & 15 → 0:8

and_not_disjoint — rule

Clearing bits the value does not have.

rule and_not_disjoint<W>(x: W, y: W) { ~x & y => y if disjoint(x, y) }
  • ~(p & 15) & (q << 4) → q << 4

or_known_ones — rule

Setting bits that are already set.

rule or_known_ones<W>(x: W, c: const W) { x | c => x if one_bits(x, c) }
  • (q | 0x81) | 1 → q | 0x81

core.arith

And plus or is plus.

identity add_and_or<W>(x: W, y: W) { (x & y) + (x | y) <=> x + y }
  • (p & q) + (p | q) → p + q

Xor plus twice the and is plus. (Twice is written as the shift mul_pow2 makes of it: patterns are written in normal form, or bottom-up rewriting never reaches them.)

identity add_xor_and<W>(x: W, y: W) { (x ^ y) + ((x & y) << 1) <=> x + y }
  • (p ^ q) + ((p & q) << 1) → p + q

or_from_xor_and — rule

Xor plus and is or (the two parts share no bits).

rule or_from_xor_and<W>(x: W, y: W) { (x ^ y) + (x & y) => x | y }
  • (p ^ q) + (p & q) → p | q

sub_and — rule

Removing the common bits.

rule sub_and<W>(x: W, y: W) { x - (x & y) => x & ~y }
  • p - (p & q) → p & ~q

neg_not — rule

-~x = x + 1.

rule neg_not<W>(x: W) { -(~x) => x + 1 }
  • -(~p) → p + 1

not_neg — rule

~-x = x - 1.

rule not_neg<W>(x: W) { ~(-x) => x - 1 }
  • ~(-p) → p - 1

neg_or_one — rule

-(x + 1) = ~x, with the increment spelled as an or.

rule neg_or_one<W>(x: W) { -(x | 1) => ~x if zero_bits(x, 1) }
  • -((p << 1) | 1) → ~(p << 1)

add_disjoint — rule

An add without carries is an or.

rule add_disjoint<W>(x: W, y: W) { x + y => x | y if disjoint(x, y) }
  • (p & 0xf0) + (q & 15) → (p & 0xf0) | (q & 15)

mul_pow2 — rule

Multiplying by a power of two is a shift.

rule mul_pow2<W>(x: W, c: const W) { x * c => x << k if is_pow2(c) let k: W = ctz(c) }
  • p * 8 → p << 3

udiv_pow2 — rule

Unsigned division by a power of two is a shift.

rule udiv_pow2<W>(x: W, c: const W) { udiv(x, c) => x >>u k if is_pow2(c) let k: W = ctz(c) }
  • udiv(p, 16) → p >>u 4

urem_pow2 — rule

Unsigned remainder by a power of two is a mask.

rule urem_pow2<W>(x: W, c: const W) { urem(x, c) => x & m if is_pow2(c) let m: W = c - 1 }
  • urem(p, 16) → p & 15

core.compare

eq_sub_zero — rule

A zero difference is equality.

rule eq_sub_zero<W>(a: W, b: W) { (a - b) == 0 => a == b }
  • p - q == 0 → p == q

eq_xor_zero — rule

A zero xor is equality.

rule eq_xor_zero<W>(a: W, b: W) { (a ^ b) == 0 => a == b }
  • (p ^ q) == 0 → p == q

ne_xor_zero — rule

A nonzero xor is inequality.

rule ne_xor_zero<W>(a: W, b: W) { (a ^ b) != 0 => a != b }
  • (p ^ q) != 0 → p != q

slt_not_not — rule

Complementing reverses the order (signed less-than).

rule slt_not_not<W>(x: W, y: W) { ~x <s ~y => y <s x }
  • ~p <s ~q → q <s p

slt_const_not — rule

Complementing reverses the order against a constant (signed less-than).

rule slt_const_not<W>(x: W, c: const W) { c <s ~x => x <s k let k: W = ~c }
  • 5 <s ~p → p <s 250

slt_not_const — rule

Complementing reverses the order against a constant (signed less-than).

rule slt_not_const<W>(x: W, c: const W) { ~x <s c => k <s x if c != 0 let k: W = ~c }
  • ~p <s 5 → 250 <s p

slt_not_zero — rule

The complement is negative when the value is not (spelled as the compares pass does).

rule slt_not_zero<W>(x: W) { ~x <s 0 => 0 <=s x }
  • ~p <s 0 → 0 <=s p

sle_not_not — rule

Complementing reverses the order (signed less-or-equal).

rule sle_not_not<W>(x: W, y: W) { ~x <=s ~y => y <=s x }
  • ~p <=s ~q → q <=s p

sle_const_not — rule

Complementing reverses the order against a constant (signed less-or-equal).

rule sle_const_not<W>(x: W, c: const W) { c <=s ~x => x <=s k let k: W = ~c }
  • 5 <=s ~p → p <=s 250

sle_not_const — rule

Complementing reverses the order against a constant (signed less-or-equal).

rule sle_not_const<W>(x: W, c: const W) { ~x <=s c => k <=s x let k: W = ~c }
  • ~p <=s 5 → 250 <=s p

ult_not_not — rule

Complementing reverses the order (unsigned less-than).

rule ult_not_not<W>(x: W, y: W) { ~x <u ~y => y <u x }
  • ~p <u ~q → q <u p

ult_const_not — rule

Complementing reverses the order against a constant (unsigned less-than).

rule ult_const_not<W>(x: W, c: const W) { c <u ~x => x <u k let k: W = ~c }
  • 5 <u ~p → p <u 250

ult_not_const — rule

Complementing reverses the order against a constant (unsigned less-than).

rule ult_not_const<W>(x: W, c: const W) { ~x <u c => k <u x let k: W = ~c }
  • ~p <u 5 → 250 <u p

ule_not_not — rule

Complementing reverses the order (unsigned less-or-equal).

rule ule_not_not<W>(x: W, y: W) { ~x <=u ~y => y <=u x }
  • ~p <=u ~q → q <=u p

ule_const_not — rule

Complementing reverses the order against a constant (unsigned less-or-equal).

rule ule_const_not<W>(x: W, c: const W) { c <=u ~x => x <=u k let k: W = ~c }
  • 5 <=u ~p → p <=u 250

ule_not_const — rule

Complementing reverses the order against a constant (unsigned less-or-equal).

rule ule_not_const<W>(x: W, c: const W) { ~x <=u c => k <=u x let k: W = ~c }
  • ~p <=u 5 → 250 <=u p

slt_or_common — rule

Bits set in both operands, outside their masks, do not decide the order (signed less-than).

rule slt_or_common<W>(x: W, y: W, m: const W, c: const W) {
        ((x & m) | c) <s ((y & m) | c) => (x & m) <s (y & m) if (m & c) == 0
    }
  • ((p & 0xfc) | 1) <s ((q & 0xfc) | 1) → (p & 0xfc) <s (q & 0xfc)

sle_or_common — rule

Bits set in both operands, outside their masks, do not decide the order (signed less-or-equal).

rule sle_or_common<W>(x: W, y: W, m: const W, c: const W) {
        ((x & m) | c) <=s ((y & m) | c) => (x & m) <=s (y & m) if (m & c) == 0
    }
  • ((p & 0xfc) | 1) <=s ((q & 0xfc) | 1) → (p & 0xfc) <=s (q & 0xfc)

ult_or_common — rule

Bits set in both operands, outside their masks, do not decide the order (unsigned less-than).

rule ult_or_common<W>(x: W, y: W, m: const W, c: const W) {
        ((x & m) | c) <u ((y & m) | c) => (x & m) <u (y & m) if (m & c) == 0
    }
  • ((p & 0xfc) | 1) <u ((q & 0xfc) | 1) → (p & 0xfc) <u (q & 0xfc)

ule_or_common — rule

Bits set in both operands, outside their masks, do not decide the order (unsigned less-or-equal).

rule ule_or_common<W>(x: W, y: W, m: const W, c: const W) {
        ((x & m) | c) <=u ((y & m) | c) => (x & m) <=u (y & m) if (m & c) == 0
    }
  • ((p & 0xfc) | 1) <=u ((q & 0xfc) | 1) → (p & 0xfc) <=u (q & 0xfc)

slt_zero_or_low — rule

Low bits set outside the mask do not change the sign.

rule slt_zero_or_low<W>(x: W, m: const W, c: const W) {
        ((x & m) | c) <s 0 => (x & m) <s 0 if (m & c) == 0 && (c & smin_lit) == 0
    }
  • ((p & 0xfc) | 1) <s 0 → (p & 0xfc) <s 0

sle_zero_or_low — rule

Low bits set outside the mask do not change the sign, and make the value nonzero.

rule sle_zero_or_low<W>(x: W, m: const W, c: const W) {
        ((x & m) | c) <=s 0 => (x & m) <s 0 if (m & c) == 0 && (c & smin_lit) == 0 && c != 0
    }
  • ((p & 0xfc) | 1) <=s 0 → (p & 0xfc) <s 0

zero_slt_or_low — rule

Low bits set outside the mask do not change the sign, and make the value nonzero.

rule zero_slt_or_low<W>(x: W, m: const W, c: const W) {
        0 <s ((x & m) | c) => 0 <=s (x & m) if (m & c) == 0 && (c & smin_lit) == 0 && c != 0
    }
  • 0 <s ((p & 0xfc) | 1) → 0 <=s (p & 0xfc)

zero_sle_or_low — rule

Low bits set outside the mask do not change the sign.

rule zero_sle_or_low<W>(x: W, m: const W, c: const W) {
        0 <=s ((x & m) | c) => 0 <=s (x & m) if (m & c) == 0 && (c & smin_lit) == 0
    }
  • 0 <=s ((p & 0xfc) | 1) → 0 <=s (p & 0xfc)

ult_one — rule

Below one is zero.

rule ult_one<W>(x: W) { x <u 1 => x == 0 }
  • p <u 1 → p == 0

eq_eq_distinct — rule

A value cannot equal two provably different values.

rule eq_eq_distinct<W>(a: W, b: W, c: W) { (a == b) & (a == c) => false if proves(b != c) }
  • (p == 1) & (p == 2) → 0:1

ult_zext — rule

Unsigned comparison of zero-extended values is the narrow comparison.

rule ult_zext<W, U>(a: W, b: W) where W < U { zext<U>(a) <u zext<U>(b) => a <u b }
  • zext<16>(p) <u zext<16>(q) → p <u q

slt_sext — rule

Signed comparison of sign-extended values is the narrow comparison.

rule slt_sext<W, U>(a: W, b: W) where W < U { sext<U>(a) <s sext<U>(b) => a <s b }
  • sext<16>(p) <s sext<16>(q) → p <s q

eq_zext — rule

Equality of extended values is equality.

rule eq_zext<W, U>(a: W, b: W) where W < U { zext<U>(a) == zext<U>(b) => a == b }
  • zext<16>(p) == zext<16>(q) → p == q

core.sign

ne_signs — rule

Two signs differ when their xor is negative.

rule ne_signs<W>(x: W, y: W) { (x <s 0) != (y <s 0) => (x ^ y) <s 0 }
  • (p <s 0) != (q <s 0) → (p ^ q) <s 0

xor_signs — rule

Two signs differ when their xor is negative.

rule xor_signs<W>(x: W, y: W) { (x <s 0) ^ (y <s 0) => (x ^ y) <s 0 }
  • (p <s 0) ^ (q <s 0) → (p ^ q) <s 0

eq_signs — rule

Two signs agree when their xor is not negative.

rule eq_signs<W>(x: W, y: W) { (x <s 0) == (y <s 0) => 0 <=s (x ^ y) }
  • (p <s 0) == (q <s 0) → 0 <=s (p ^ q)

and_signs — rule

Both are negative when their and is.

rule and_signs<W>(x: W, y: W) { (x <s 0) & (y <s 0) => (x & y) <s 0 }
  • (p <s 0) & (q <s 0) → (p & q) <s 0

or_signs — rule

Either is negative when their or is.

rule or_signs<W>(x: W, y: W) { (x <s 0) | (y <s 0) => (x | y) <s 0 }
  • (p <s 0) | (q <s 0) → (p | q) <s 0

lshr_sign_eq_one — rule

The sign bit shifted down is one when the value is negative.

rule lshr_sign_eq_one<W>(x: W) where 1 < W { x >>u (W - 1) == 1 => x <s 0 }
  • p >>u 7 == 1 → p <s 0

lshr_sign_ne_zero — rule

The sign bit shifted down is nonzero when the value is negative.

rule lshr_sign_ne_zero<W>(x: W) { x >>u (W - 1) != 0 => x <s 0 }
  • p >>u 7 != 0 → p <s 0

xor_lshr_signs — rule

The xor of two sign bits shifted down is the sign bit of the xor.

rule xor_lshr_signs<W>(x: W, y: W) {
        (x >>u (W - 1)) ^ (y >>u (W - 1)) => (x ^ y) >>u (W - 1)
    }
  • (p >>u 7) ^ (q >>u 7) → (p ^ q) >>u 7

ne_sign_bits — rule

Two sign bits differ when the xor’s sign bit is set.

rule ne_sign_bits<W>(x: W, y: W) {
        extract<W - 1, 1>(x) != extract<W - 1, 1>(y) => extract<W - 1, 1>(x ^ y)
    }
  • extract<7, 1>(p) != extract<7, 1>(q) → extract<7, 1>(p ^ q)

xor_sign_bits — rule

The xor of two sign bits is the sign bit of the xor.

rule xor_sign_bits<W>(x: W, y: W) {
        extract<W - 1, 1>(x) ^ extract<W - 1, 1>(y) => extract<W - 1, 1>(x ^ y)
    }
  • extract<7, 1>(p) ^ extract<7, 1>(q) → extract<7, 1>(p ^ q)

slt_from_flags — rule

SF != OF after a - b (x86 setl, AArch64 lt) is signed less-than.

rule slt_from_flags<W>(a: W, b: W) {
        ((a - b) ^ (((a - b) ^ a) & (a ^ b))) <s 0 => a <s b
    }
  • (p - q ^ ((p - q ^ p) & (p ^ q))) <s 0 → p <s q

sge_from_flags — rule

SF == OF after a - b (x86 setge, AArch64 ge) is signed greater-or-equal.

rule sge_from_flags<W>(a: W, b: W) {
        0 <=s ((a - b) ^ (((a - b) ^ a) & (a ^ b))) => b <=s a
    }
  • 0 <=s (p - q ^ ((p - q ^ p) & (p ^ q))) → q <=s p

slt_from_flags_const — rule

SF != OF after a - k for a constant k (whose subtraction is built as a + -k).

rule slt_from_flags_const<W>(a: W, c: const W, k: const W) {
        ((a + c) ^ (((a + c) ^ a) & (a ^ k))) <s 0 => a <s k
        if c + k == 0
    }
  • (p + 0xfb ^ ((p + 0xfb ^ p) & (p ^ 5))) <s 0 → p <s 5

sge_from_flags_const — rule

SF == OF after a - k for a constant k.

rule sge_from_flags_const<W>(a: W, c: const W, k: const W) {
        0 <=s ((a + c) ^ (((a + c) ^ a) & (a ^ k))) => k <=s a
        if c + k == 0
    }
  • 0 <=s (p + 0xfb ^ ((p + 0xfb ^ p) & (p ^ 5))) → 5 <=s p

slt_from_flags_word — rule

SF != OF as a 0 or 1 word.

rule slt_from_flags_word<W>(a: W, b: W) where 1 < W {
        ((a - b) ^ (((a - b) ^ a) & (a ^ b))) >>u (W - 1) => zext<W>(a <s b)
    }
  • (p - q ^ ((p - q ^ p) & (p ^ q))) >>u 7 → zext<8>(p <s q)

slt_from_flag_bits — rule

SF != OF as a bit.

rule slt_from_flag_bits<W>(a: W, b: W) {
        extract<W - 1, 1>((a - b) ^ (((a - b) ^ a) & (a ^ b))) => a <s b
    }
  • extract<7, 1>(p - q ^ ((p - q ^ p) & (p ^ q))) → p <s q

sge_from_flag_bits — rule

SF == OF as bits.

rule sge_from_flag_bits<W>(a: W, b: W) {
        extract<W - 1, 1>(a - b) == extract<W - 1, 1>(((a - b) ^ a) & (a ^ b)) => b <=s a
    }
  • extract<7, 1>(p - q) == extract<7, 1>((p - q ^ p) & (p ^ q)) → q <=s p

core.casts

zext_trunc — rule

Truncate, then zero-extend back: a mask.

rule zext_trunc<W, N>(x: W) where N < W { zext<W>(trunc<N>(x)) => x & lowmask(N) }
  • zext<8>(trunc<4>(p)) → p & 15

sext_from_sign_word — rule

Sign extension spelled as “value | (sign word << W)”. Needs U <= 2W to be exact.

rule sext_from_sign_word<W, U>(a: W) where W < U, U <= 2 * W {
        zext<U>(a) | (zext<U>(a >>s (W - 1)) << W) => sext<U>(a)
    }
  • zext<16>(p) | (zext<16>(p >>s 7) << 8) → sext<16>(p)

core.select

select_one_zero — rule

A condition selecting one or zero is its zero extension.

rule select_one_zero<W>(c: 1) where 1 < W { select(c, 1, 0) => zext<W>(c) }
  • select(p <u q, 1:16, 0:16) → zext<16>(p <u q)

select_ones_zero — rule

A condition selecting all ones or zero is its sign extension.

rule select_ones_zero<W>(c: 1) where 1 < W { select(c, ones, 0) => sext<W>(c) }
  • select(p <u q, 0xffff:16, 0:16) → sext<16>(p <u q)

core.shift

shl_shl — rule

Two left shifts by constants that do not overflow the width.

rule shl_shl<W>(x: W, a: const W, b: const W) {
        (x << a) << b => x << k
        if (a <u W) && (b <u W) && (a + b <u W)
        let k: W = a + b
    }
  • (p << 2) << 3 → p << 5

lshr_lshr — rule

Two logical right shifts by constants that do not overflow the width.

rule lshr_lshr<W>(x: W, a: const W, b: const W) {
        (x >>u a) >>u b => x >>u k
        if (a <u W) && (b <u W) && (a + b <u W)
        let k: W = a + b
    }
  • (p >>u 2) >>u 3 → p >>u 5

lshr_shl — rule

Shifting right then left by the same amount clears the low bits.

rule lshr_shl<W>(x: W, c: const W) {
        (x >>u c) << c => x & m
        if c <u W
        let m: W = ones << c
    }
  • (p >>u 3) << 3 → p & 0xf8

rotl_rotl — rule

Two rotations by constants.

rule rotl_rotl<W>(x: W, a: const W, b: const W) {
        rotl(rotl(x, a), b) => rotl(x, k)
        let k: W = urem(urem(a, W) + urem(b, W), W)
    }
  • rotl(rotl(p, 3), 7) → rotl(p, 2)

rotl_rotr — rule

Rotating back by the same count, whatever it is.

rule rotl_rotr<W>(x: W, y: W) { rotl(rotr(x, y), y) => x }
  • rotl(rotr(p, q), q) → p

rotr_rotl — rule

Rotating back by the same count, whatever it is.

rule rotr_rotl<W>(x: W, y: W) { rotr(rotl(x, y), y) => x }
  • rotr(rotl(p, q), q) → p

core.recovery_arith

xor_from_or_minus_and — rule

rule xor_from_or_minus_and<W>(x: W, y: W) { (x | y) - (x & y) => x ^ y }
  • (p | q) - (p & q) → p ^ q

xor_from_sum_double_and — rule

rule xor_from_sum_double_and<W>(x: W, y: W) { (x + y) - ((x & y) << 1) => x ^ y }
  • (p + q) - ((p & q) << 1) → p ^ q

or_from_sum_minus_and — rule

rule or_from_sum_minus_and<W>(x: W, y: W) { (x + y) - (x & y) => x | y }
  • (p + q) - (p & q) → p | q

and_from_sum_minus_or — rule

rule and_from_sum_minus_or<W>(x: W, y: W) { (x + y) - (x | y) => x & y }
  • (p + q) - (p | q) → p & q

and_from_or_minus_xor — rule

rule and_from_or_minus_xor<W>(x: W, y: W) { (x | y) - (x ^ y) => x & y }
  • (p | q) - (p ^ q) → p & q

xor_from_double_or_sum — rule

rule xor_from_double_or_sum<W>(x: W, y: W) { ((x | y) << 1) - (x + y) => x ^ y }
  • ((p | q) << 1) - (p + q) → p ^ q

sum_from_double_or_xor — rule

rule sum_from_double_or_xor<W>(x: W, y: W) { ((x | y) << 1) - (x ^ y) => x + y }
  • ((p | q) << 1) - (p ^ q) → p + q

negative_double — rule

rule negative_double<W>(x: W) { -x - x => x * k let k: W = ones - 1 }
  • -p - p → p * 254

core.recovery_compare

eq_complement — rule

rule eq_complement<W>(x: W) { x == ~x => false }
  • p == ~p → 0:1

ne_complement — rule

rule ne_complement<W>(x: W) { x != ~x => true }
  • p != ~p → 1:1

ule_add_carry — rule

rule ule_add_carry<W>(x: W, y: W) { (x + y) <=u y => -x <=u y }
  • (p + q) <=u q → -p <=u q

ugt_add_no_carry — rule

rule ugt_add_no_carry<W>(x: W, y: W) { y <u (x + y) => y <u -x }
  • q <u (p + q) → q <u -p

uge_add_no_carry — rule

rule uge_add_no_carry<W>(x: W, y: W) { y <=u (x + y) => y <=u ~x }
  • q <=u (p + q) → q <=u ~p

ule_sub_no_borrow — rule

rule ule_sub_no_borrow<W>(x: W, y: W) { (x - y) <=u ~y => y <=u x }
  • (p - q) <=u ~q → q <=u p

ugt_sub_borrow — rule

rule ugt_sub_borrow<W>(x: W, y: W) { ~y <u (x - y) => x <u y }
  • ~q <u (p - q) → p <u q

ne_sub_zero — rule

rule ne_sub_zero<W>(x: W, y: W) { (x - y) != 0 => x != y }
  • (p - q) != 0 → p != q

core.recovery_shift

shl_lshr — rule

rule shl_lshr<W>(x: W, c: const W) { (x << c) >>u c => x & m let m: W = ones >>u c }
  • (p << 1) >>u 1 → p & 127

ashr_ashr — rule

rule ashr_ashr<W>(x: W, a: const W, b: const W) {
        (x >>s a) >>s b => x >>s k
        let k: W = umin(umin(a, W - 1) + umin(b, W - 1), W - 1)
    }
  • (p >>s 7) >>s 7 → p >>s 7

shl_xor_dead_constant — rule

rule shl_xor_dead_constant<W>(x: W, c: const W, k: const W) {
        (x ^ c) << k => x << k if (c << k) == 0
    }
  • (p ^ 128) << 7 → p << 7

lshr_xor_dead_constant — rule

rule lshr_xor_dead_constant<W>(x: W, c: const W, k: const W) {
        (x ^ c) >>u k => x >>u k if (c >>u k) == 0
    }
  • (p ^ 1) >>u 1 → p >>u 1

negate_sign_bit — rule

rule negate_sign_bit<W>(x: W) { -(x >>u (W - 1)) => x >>s (W - 1) }
  • -(p >>u 7) → p >>s 7

nonzero_from_sign_union — rule

rule nonzero_from_sign_union<W>(x: W) where 1 < W {
        (x | -x) >>u (W - 1) => zext<W>(x != 0)
    }
  • (p | -p) >>u 7 → zext<8>(p != 0)

core.recovery_bmi2

pdep_pext — rule

rule pdep_pext<W>(x: W, m: W) { pdep(pext(x, m), m) => x & m }
  • pdep(pext(p, q), q) → p & q

pext_pdep — rule

rule pext_pdep<W>(x: W, m: const W) {
        pext(pdep(x, m), m) => x & k let k: W = ones >>u (W - popcnt(m))
    }
  • pext(pdep(p, 0x55), 0x55) → p & 15

pdep_ones — rule

rule pdep_ones<W>(m: W) { pdep(ones, m) => m }
  • pdep(255, p) → p

pext_complement_mask — rule

rule pext_complement_mask<W>(m: W) { pext(~m, m) => 0 }
  • pext(~p, p) → 0:8

pdep_outside_mask — rule

rule pdep_outside_mask<W>(x: W, m: W) { pdep(x, m) & ~m => 0 }
  • pdep(p, q) & ~q → 0:8

pdep_inside_mask — rule

rule pdep_inside_mask<W>(x: W, m: W) { pdep(x, m) & m => pdep(x, m) }
  • pdep(p, q) & q → pdep(p, q)

pdep_pext_and — rule

rule pdep_pext_and<W>(x: W, y: W, m: W) { pdep(pext(x, m) & pext(y, m), m) => (x & y) & m }
  • pdep(pext(p, r) & pext(q, r), r) → (p & q) & r

pdep_pext_or — rule

rule pdep_pext_or<W>(x: W, y: W, m: W) { pdep(pext(x, m) | pext(y, m), m) => (x | y) & m }
  • pdep(pext(p, r) | pext(q, r), r) → (p | q) & r

pdep_pext_xor — rule

rule pdep_pext_xor<W>(x: W, y: W, m: W) { pdep(pext(x, m) ^ pext(y, m), m) => (x ^ y) & m }
  • pdep(pext(p, r) ^ pext(q, r), r) → (p ^ q) & r

core.recovery_cast

add_trunc_pair — rule

rule add_trunc_pair<W, N>(x: W, y: W) where N < W {
        trunc<N>(x) + trunc<N>(y) => trunc<N>(x + y)
    }
  • trunc<4>(p) + trunc<4>(q) → trunc<4>(p + q)

sub_trunc_pair — rule

rule sub_trunc_pair<W, N>(x: W, y: W) where N < W {
        trunc<N>(x) - trunc<N>(y) => trunc<N>(x - y)
    }
  • trunc<4>(p) - trunc<4>(q) → trunc<4>(p - q)

mul_trunc_pair — rule

rule mul_trunc_pair<W, N>(x: W, y: W) where N < W {
        trunc<N>(x) * trunc<N>(y) => trunc<N>(x * y)
    }
  • trunc<4>(p) * trunc<4>(q) → trunc<4>(p * q)

core.recovery_select

select_nonzero_self — rule

rule select_nonzero_self<W>(x: W) { select(0 <u x, x, 0) => x }
  • select(0 <u p, p, 0) → p

merge_complementary_select_or — rule

rule merge_complementary_select_or<W>(c: 1, x: W, y: W) {
        select(c, x, 0) | select(c, 0, y) => select(c, x, y)
    }
  • select(p == 0, q, 0) | select(p == 0, 0, r) → select(p == 0, q, r)

merge_complementary_select_add — rule

rule merge_complementary_select_add<W>(c: 1, x: W, y: W) {
        select(c, x, 0) + select(c, 0, y) => select(c, x, y)
    }
  • select(p == 0, q, 0) + select(p == 0, 0, r) → select(p == 0, q, r)

merge_complementary_zext_or — rule

rule merge_complementary_zext_or<W>(c: 1, y: W) where 1 < W {
        zext<W>(c) | select(c, 0, y) => select(c, 1, y)
    }
  • zext<8>(p == 0) | select(p == 0, 0, r) → select(p == 0, 1:8, r)

merge_complementary_zext_add — rule

rule merge_complementary_zext_add<W>(c: 1, y: W) where 1 < W {
        zext<W>(c) + select(c, 0, y) => select(c, 1, y)
    }
  • zext<8>(p == 0) + select(p == 0, 0, r) → select(p == 0, 1:8, r)

merge_complementary_sext_or — rule

rule merge_complementary_sext_or<W>(c: 1, y: W) where 1 < W {
        sext<W>(c) | select(c, 0, y) => select(c, ones, y)
    }
  • sext<8>(p == 0) | select(p == 0, 0, r) → select(p == 0, 255:8, r)

merge_complementary_sext_add — rule

rule merge_complementary_sext_add<W>(c: 1, y: W) where 1 < W {
        sext<W>(c) + select(c, 0, y) => select(c, ones, y)
    }
  • sext<8>(p == 0) + select(p == 0, 0, r) → select(p == 0, 255:8, r)

core.recovery_canonical

xor_sign_bit_add — rule

rule xor_sign_bit_add<W>(x: W) { (x ^ bit(W - 1)) + bit(W - 1) => x }
  • (p ^ 128) + 128 → p

ule_constant_borrow — rule

rule ule_constant_borrow<W>(x: W, c: const W, m: const W) {
        (x + c) <=u m => k <=u x if m == c - 1 let k: W = -c
    }
  • p + 255 <=u 254 → 1 <=u p

ugt_constant_borrow — rule

rule ugt_constant_borrow<W>(x: W, c: const W, m: const W) {
        m <u (x + c) => x <u k if m == c - 1 let k: W = -c
    }
  • 254 <u p + 255 → p <u 1

pdep_pext_xor_constant — rule

rule pdep_pext_xor_constant<W>(x: W, c: const W, m: const W) {
        pdep(pext(x, m) ^ c, m) => (x ^ k) & m let k: W = pdep(c, m)
    }
  • pdep(pext(p, 1) ^ 1, 1) → (p ^ 1) & 1

core.recovery_minmax

smin_select_or — rule

rule smin_select_or<W>(x: W, y: W) { select(x <=s y, x, 0) | select(y <s x, y, 0) => smin(x, y) }
  • select(p <=s q, p, 0) | select(q <s p, q, 0) → smin(p, q)

smin_select_add — rule

rule smin_select_add<W>(x: W, y: W) { select(x <=s y, x, 0) + select(y <s x, y, 0) => smin(x, y) }
  • select(p <=s q, p, 0) + select(q <s p, q, 0) → smin(p, q)

smax_select_or — rule

rule smax_select_or<W>(x: W, y: W) { select(y <=s x, x, 0) | select(x <s y, y, 0) => smax(x, y) }
  • select(q <=s p, p, 0) | select(p <s q, q, 0) → smax(p, q)

smax_select_add — rule

rule smax_select_add<W>(x: W, y: W) { select(y <=s x, x, 0) + select(x <s y, y, 0) => smax(x, y) }
  • select(q <=s p, p, 0) + select(p <s q, q, 0) → smax(p, q)

umin_select_or — rule

rule umin_select_or<W>(x: W, y: W) { select(x <=u y, x, 0) | select(y <u x, y, 0) => umin(x, y) }
  • select(p <=u q, p, 0) | select(q <u p, q, 0) → umin(p, q)

umin_select_add — rule

rule umin_select_add<W>(x: W, y: W) { select(x <=u y, x, 0) + select(y <u x, y, 0) => umin(x, y) }
  • select(p <=u q, p, 0) + select(q <u p, q, 0) → umin(p, q)

umax_select_or — rule

rule umax_select_or<W>(x: W, y: W) { select(y <=u x, x, 0) | select(x <u y, y, 0) => umax(x, y) }
  • select(q <=u p, p, 0) | select(p <u q, q, 0) → umax(p, q)

umax_select_add — rule

rule umax_select_add<W>(x: W, y: W) { select(y <=u x, x, 0) + select(x <u y, y, 0) => umax(x, y) }
  • select(q <=u p, p, 0) + select(p <u q, q, 0) → umax(p, q)

umax_ones_sext_or — rule

rule umax_ones_sext_or<W>(x: W) where 1 < W {
        sext<W>(x <u ones) | select(ones <=u x, x, 0) => ones
    }
  • sext<8>(p <u 255) | select(255 <=u p, p, 0) → 255:8

umax_ones_sext_add — rule

rule umax_ones_sext_add<W>(x: W) where 1 < W {
        sext<W>(x <u ones) + select(ones <=u x, x, 0) => ones
    }
  • sext<8>(p <u 255) + select(255 <=u p, p, 0) → 255:8

core.recovery_select_one

smin_one_or — rule

rule smin_one_or<W>(x: W) where 1 < W { zext<W>(1 <s x) | select(x <=s 1, x, 0) => smin(x, 1) }
  • zext<8>(1 <s p) | select(p <=s 1, p, 0) → smin(p, 1)

smin_one_add — rule

rule smin_one_add<W>(x: W) where 1 < W { zext<W>(1 <s x) + select(x <=s 1, x, 0) => smin(x, 1) }
  • zext<8>(1 <s p) + select(p <=s 1, p, 0) → smin(p, 1)

smax_one_or — rule

rule smax_one_or<W>(x: W) where 1 < W { zext<W>(x <s 1) | select(1 <=s x, x, 0) => smax(x, 1) }
  • zext<8>(p <s 1) | select(1 <=s p, p, 0) → smax(p, 1)

smax_one_add — rule

rule smax_one_add<W>(x: W) where 1 < W { zext<W>(x <s 1) + select(1 <=s x, x, 0) => smax(x, 1) }
  • zext<8>(p <s 1) + select(1 <=s p, p, 0) → smax(p, 1)

umin_one_or — rule

rule umin_one_or<W>(x: W) where 1 < W { zext<W>(1 <u x) | select(x <=u 1, x, 0) => umin(x, 1) }
  • zext<8>(1 <u p) | select(p <=u 1, p, 0) → umin(p, 1)

umin_one_add — rule

rule umin_one_add<W>(x: W) where 1 < W { zext<W>(1 <u x) + select(x <=u 1, x, 0) => umin(x, 1) }
  • zext<8>(1 <u p) + select(p <=u 1, p, 0) → umin(p, 1)

umax_one_or — rule

rule umax_one_or<W>(x: W) where 1 < W { zext<W>(x == 0) | select(1 <=u x, x, 0) => umax(x, 1) }
  • zext<8>(p == 0) | select(1 <=u p, p, 0) → umax(p, 1)

umax_one_add — rule

rule umax_one_add<W>(x: W) where 1 < W { zext<W>(x == 0) + select(1 <=u x, x, 0) => umax(x, 1) }
  • zext<8>(p == 0) + select(1 <=u p, p, 0) → umax(p, 1)

smin_one_first_or — rule

rule smin_one_first_or<W>(x: W) where 1 < W { zext<W>(1 <=s x) | select(x <s 1, x, 0) => smin(x, 1) }
  • zext<8>(1 <=s p) | select(p <s 1, p, 0) → smin(p, 1)

smin_one_first_add — rule

rule smin_one_first_add<W>(x: W) where 1 < W { zext<W>(1 <=s x) + select(x <s 1, x, 0) => smin(x, 1) }
  • zext<8>(1 <=s p) + select(p <s 1, p, 0) → smin(p, 1)

smax_one_first_or — rule

rule smax_one_first_or<W>(x: W) where 1 < W { zext<W>(x <=s 1) | select(1 <s x, x, 0) => smax(x, 1) }
  • zext<8>(p <=s 1) | select(1 <s p, p, 0) → smax(p, 1)

smax_one_first_add — rule

rule smax_one_first_add<W>(x: W) where 1 < W { zext<W>(x <=s 1) + select(1 <s x, x, 0) => smax(x, 1) }
  • zext<8>(p <=s 1) + select(1 <s p, p, 0) → smax(p, 1)

umax_one_first_or — rule

rule umax_one_first_or<W>(x: W) where 1 < W { zext<W>(x <=u 1) | select(1 <u x, x, 0) => select(1 <u x, x, 1) }
  • zext<8>(p <=u 1) | select(1 <u p, p, 0) → select(1 <u p, p, 1)

umax_one_first_add — rule

rule umax_one_first_add<W>(x: W) where 1 < W { zext<W>(x <=u 1) + select(1 <u x, x, 0) => select(1 <u x, x, 1) }
  • zext<8>(p <=u 1) + select(1 <u p, p, 0) → select(1 <u p, p, 1)

core.recovery_shuffle

concat_low_zero — rule

rule concat_low_zero<N, U>(x: N + U, z: const U) where N + U <= 512 {
        concat(trunc<N>(x), z) => x << U if z == 0
    }
  • concat(trunc<1>(p), 0:7) → p << 7

core.recovery_select_ones

smin_ones_or — rule

rule smin_ones_or<W>(x: W) where 1 < W { sext<W>(ones <s x) | select(x <=s ones, x, 0) => smin(x, ones) }
  • sext<8>(255 <s p) | select(p <=s 255, p, 0) → smin(p, 255)

smin_ones_add — rule

rule smin_ones_add<W>(x: W) where 1 < W { sext<W>(ones <s x) + select(x <=s ones, x, 0) => smin(x, ones) }
  • sext<8>(255 <s p) + select(p <=s 255, p, 0) → smin(p, 255)

smax_ones_or — rule

rule smax_ones_or<W>(x: W) where 1 < W { sext<W>(x <s ones) | select(ones <=s x, x, 0) => smax(x, ones) }
  • sext<8>(p <s 255) | select(255 <=s p, p, 0) → smax(p, 255)

smax_ones_add — rule

rule smax_ones_add<W>(x: W) where 1 < W { sext<W>(x <s ones) + select(ones <=s x, x, 0) => smax(x, ones) }
  • sext<8>(p <s 255) + select(255 <=s p, p, 0) → smax(p, 255)

smin_ones_first_or — rule

rule smin_ones_first_or<W>(x: W) where 1 < W { sext<W>(ones <=s x) | select(x <s ones, x, 0) => smin(x, ones) }
  • sext<8>(255 <=s p) | select(p <s 255, p, 0) → smin(p, 255)

smin_ones_first_add — rule

rule smin_ones_first_add<W>(x: W) where 1 < W { sext<W>(ones <=s x) + select(x <s ones, x, 0) => smin(x, ones) }
  • sext<8>(255 <=s p) + select(p <s 255, p, 0) → smin(p, 255)

smax_ones_first_or — rule

rule smax_ones_first_or<W>(x: W) where 1 < W { sext<W>(x <=s ones) | select(ones <s x, x, 0) => smax(x, ones) }
  • sext<8>(p <=s 255) | select(255 <s p, p, 0) → smax(p, 255)

smax_ones_first_add — rule

rule smax_ones_first_add<W>(x: W) where 1 < W { sext<W>(x <=s ones) + select(ones <s x, x, 0) => smax(x, ones) }
  • sext<8>(p <=s 255) + select(255 <s p, p, 0) → smax(p, 255)

umin_ones_first_add — rule

rule umin_ones_first_add<W>(x: W) where 1 < W { select(x <u ones, x, 0) + sext<W>(ones <=u x) => x }
  • select(p <u 255, p, 0) + sext<8>(255 <=u p) → p

umin_ones_first_or — rule

rule umin_ones_first_or<W>(x: W) where 1 < W { select(x <u ones, x, 0) | sext<W>(ones <=u x) => x }
  • select(p <u 255, p, 0) | sext<8>(255 <=u p) → p

core.guarded_recovery

slt_neg_pair — rule

rule slt_neg_pair<W>(x: W, y: W) {
        -x <s -y => y <s x if proves(x != bit(W - 1)) && proves(y != bit(W - 1))
    }
  • -((p & 252) | 2) <s -((q & 252) | 2) → ((q & 252) | 2) <s ((p & 252) | 2)

sle_neg_pair — rule

rule sle_neg_pair<W>(x: W, y: W) {
        -x <=s -y => y <=s x if proves(x != bit(W - 1)) && proves(y != bit(W - 1))
    }
  • -((p & 252) | 2) <=s -((q & 252) | 2) → ((q & 252) | 2) <=s ((p & 252) | 2)

select_nonzero_value — rule

rule select_nonzero_value<W>(x: W) { select(x != 0, x, 0) => x }
  • select(p != 0, p, 0) → p

negate_boolean — rule

rule negate_boolean(x: 1) { -x => x }
  • -p:1 → p:1

increment_boolean — rule

rule increment_boolean(x: 1) { x + 1 => ~x }
  • p:1 + 1:1 → ~p:1

complementary_shift_rotate — rule

rule complementary_shift_rotate<W>(x: W, a: const W, b: const W) {
        (x >>u a) | (x << b) => rotr(x, a)
        if (a <u W) && (b <u W) && (a + b == W)
    }
  • (p >>u 3) | (p << 5) → rotr(p, 3)

xor_zext_lowmask — rule

rule xor_zext_lowmask<W, U>(x: W) where W < U {
        zext<U>(x) ^ lowmask(W) => zext<U>(~x)
    }
  • zext<16>(p) ^ 255:16 → zext<16>(~p)

xor_complement_mask — rule

rule xor_complement_mask<W>(x: W, m: const W, n: const W) {
        (x & n) ^ m => x | m if n == ~m
    }
  • (p & 254) ^ 1 → p | 1

mask_or_add_constant — rule

rule mask_or_add_constant<W>(x: W, c: const W, m: const W, d: const W) {
        ((x | c) & m) + d => (x & kept) | k
        if (((c & m) + d) & (m & ~c)) == 0
        let kept: W = m & ~c
        let k: W = (c & m) + d
    }
  • ((p | 3) & 15) + 255 → (p & 12) | 2

mask_or_add_constant_carry — rule

rule mask_or_add_constant_carry<W>(x: W, c: const W, m: const W, d: const W) {
        ((x | c) & m) + d => (x & kept) + k
        let kept: W = m & ~c
        let k: W = (c & m) + d
    }
  • ((p | 127) & 0xf0) + 16 → (p & 128) + 128

boolean_lowbit_mask_keep — rule

rule boolean_lowbit_mask_keep<W>(x: W, y: W) where 1 < W {
        -(x & 1) & y => select(trunc<1>(x), y, 0)
    }
  • -(p & 1) & q → select(trunc<1>(p), q, 0)

boolean_zext_mask_keep — rule

rule boolean_zext_mask_keep<W>(x: 1, y: W) where 1 < W {
        -zext<W>(x) & y => select(x, y, 0)
    }
  • -zext<8>(p:1) & q → select(p:1, q, 0)

split_mask_add — rule

rule split_mask_add<W>(x: W, c: W, m: const W, n: const W) {
        (x & m) + ((x & n) + c) => (x & k) + c if (m & n) == 0 let k: W = m | n
    }
  • (p & 240) + ((p & 15) + q) → p + q

split_mask_or_or — rule

rule split_mask_or_or<W>(x: W, c: W, m: const W, n: const W) {
        (x & m) | ((x & n) | c) => (x & k) | c let k: W = m | n
    }
  • (p & 240) | ((p & 2) | 1) → (p & 242) | 1

split_mask_sub — rule

rule split_mask_sub<W>(x: W, c: W, m: const W, n: const W) {
        (x & m) + ((x & n) - c) => x - c if (m | n) == ones && (m & n) == 0
    }
  • (p & 240) + ((p & 15) - q) → p - q

split_mask_increment — rule

rule split_mask_increment<W>(x: W, m: const W, n: const W) {
        (x & m) - ~(x & n) => x + 1 if (m | n) == ones && (m & n) == 0
    }
  • (p & 240) - ~(p & 15) → p + 1

recombine_shifted_field_or — rule

rule recombine_shifted_field_or<W>(x: W, a: W, m: const W, k: const W) {
        (x & m) | (((x >>u k) + a) << k) => (x & keep) + (a << k)
        if (k <=u W) && (m & (ones << k)) == 0
        let keep: W = (ones << k) | m
    }
  • (p & 15) | (((p >>u 4) + q) << 4) → p + (q << 4)

core.guarded_recovery_canonical

slt_neg_zero — rule

rule slt_neg_zero<W>(x: W) { -x <s 0 => 0 <s x if proves(x != bit(W - 1)) }
  • -((p & 252) | 2) <s 0 → 0 <s ((p & 252) | 2)

slt_zero_neg — rule

rule slt_zero_neg<W>(x: W) { 0 <s -x => x <s 0 if proves(x != bit(W - 1)) }
  • 0 <s -((p & 252) | 2) → ((p & 252) | 2) <s 0

sle_neg_zero — rule

rule sle_neg_zero<W>(x: W) { -x <=s 0 => 0 <=s x if proves(x != bit(W - 1)) }
  • -((p & 252) | 2) <=s 0 → 0 <=s ((p & 252) | 2)

sle_zero_neg — rule

rule sle_zero_neg<W>(x: W) { 0 <=s -x => x <=s 0 if proves(x != bit(W - 1)) }
  • 0 <=s -((p & 252) | 2) → ((p & 252) | 2) <=s 0

split_mask_or_add — rule

rule split_mask_or_add<W>(x: W, c: W, m: const W, n: const W) {
        (x & m) + ((x & n) | c) => x + c
        if (m | n) == ones && (m & n) == 0 && zero_bits(c, n)
    }
  • (p & 240) + ((p & 15) | 128) → p + 128

core.guarded_recovery_shuffle

recombine_extracted_field — rule

rule recombine_extracted_field<N, K, M>(x: N + K, a: N + K) where M <= N, N + K <= 512 {
        concat(trunc<N>(zext<N + K>(extract<K, M>(x)) + a), trunc<K>(x))
        => (x & lowmask(K + M)) + (a << K)
    }
  • concat(trunc<4>(zext<8>(extract<4,3>(p)) + q), trunc<4>(p)) → (p & 127) + (q << 4)

recombine_extracted_field_or — rule

rule recombine_extracted_field_or<N, K, M>(x: N + K, a: N + K) where M <= N, N + K <= 512 {
        concat(trunc<N>(zext<N + K>(extract<K, M>(x)) | a), trunc<K>(x))
        => (x & lowmask(K + M)) | (a << K)
    }
  • concat(trunc<4>(zext<8>(extract<4,3>(p)) | q), trunc<4>(p)) → (p & 127) | (q << 4)

core.guarded_recovery_layout

shifted_masked_add — rule

rule shifted_masked_add<W>(x: W, a: W, s: const W, k: const W) {
        (((x >>u k) & s) + a) << k => (x & keep) + (a << k)
        let keep: W = s << k
    }
  • (((p >>u 4) & 8) + q) << 4 → (p & 128) + (q << 4)

shifted_add_constant — rule

rule shifted_add_constant<W>(x: W, c: const W, k: const W) {
        ((x >>u k) + c) << k => (x & keep) + d
        let keep: W = ones << k
        let d: W = c << k
    }
  • ((p >>u 4) + 3) << 4 → (p & 240) + 48

shifted_or_constant — rule

rule shifted_or_constant<W>(x: W, c: const W, k: const W) {
        ((x >>u k) | c) << k => (x & keep) | d
        let keep: W = ones << k
        let d: W = c << k
    }
  • ((p >>u 4) | 3) << 4 → (p & 240) | 48

recombine_low_constant — rule

rule recombine_low_constant<W>(x: W, a: W, c: const W, k: const W) {
        (((x >>u k) + a) << k) | c => (x | c) + (a << k)
        if (k <=u W) && c == ones >>u (W - k)
    }
  • (((p >>u 4) + q) << 4) | 15 → (p | 15) + (q << 4)

split_or_masked_sum — rule

rule split_or_masked_sum<W>(x: W, c: W, m: const W, n: const W) {
        (x & m) | ((x & n) + c) => (x & keep) + c
        if (n & prefix) == 0 && zero_bits(c, prefix)
        let prefix: W = ones >>u clz(m)
        let keep: W = m | n
    }
  • (p & 14) | ((p & 240) + (q << 4)) → (p & 254) + (q << 4)

add_constant_to_concat — rule

rule add_constant_to_concat<H, L>(x: H + L, c: const L, d: const H + L) where H + L <= 512 {
        concat(extract<L,H>(x), c) + d => (x & ~lowmask(L)) + k
        let k: H + L = zext<H + L>(c) + d
    }
  • concat(extract<7,1>(p), 112:7) + 16 → (p & 128) + 128

or_constant_to_concat — rule

rule or_constant_to_concat<H, L>(x: H + L, c: const L, d: const H + L) where H + L <= 512 {
        concat(extract<L,H>(x), c) | d => (x & ~lowmask(L)) | k
        let k: H + L = zext<H + L>(c) | d
    }
  • concat(extract<7,1>(p), 112:7) | 3 → (p & 128) | 115

core.guarded_recovery_mba_normalized

xor_from_distributed_double_or — rule

rule xor_from_distributed_double_or<W>(x: W, a: const W, c: const W) {
        ((x | a) << 1) - (x + c) => x ^ c if (a << 1) == (c << 1)
    }
  • ((p | 1) << 1) - (p + 129) → p ^ 129

sum_from_distributed_double_or — rule

rule sum_from_distributed_double_or<W>(x: W, a: const W, c: const W) {
        ((x | a) << 1) - (x ^ c) => x + c if (a << 1) == (c << 1)
    }
  • ((p | 1) << 1) - (p ^ 129) → p + 129

sum_from_vanishing_double_mask — rule

rule sum_from_vanishing_double_mask<W>(x: W, c: const W) {
        (x << 1) - (x ^ c) => x + c if (c << 1) == 0
    }
  • (p << 1) - (p ^ 128) → p + 128

core.guarded_recovery_concat

add_aligned_to_concat — rule

rule add_aligned_to_concat<H, L>(a: H + L, x: H + L, c: const L) where H + L <= 512 {
        (a << L) + concat(extract<L,H>(x), c)
        => concat(trunc<H>(a) + extract<L,H>(x), c)
    }
  • (q << 4) + concat(extract<4,4>(p), 15:4) → concat(trunc<4>(q) + extract<4,4>(p), 15:4)

core.shift_canonical

shl_mask_dead — rule

A mask that keeps every bit the shift keeps.

rule shl_mask_dead<W>(x: W, c: const W, k: const W) {
        (x & c) << k => x << k if (c | ~(ones >>u k)) == ones
    }
  • (p & 63) << 2 → p << 2

and_shl_const — rule

rule and_shl_const<W>(x: W, k: const W, m: const W) {
        (x << k) & m => (x & c) << k if one_use(x << k) let c: W = m >>u k
    }
  • (p << 2) & 60 → (p & 15) << 2

and_double_const — rule

rule and_double_const<W>(x: W, m: const W) {
        (x + x) & m => (x & c) << 1 if one_use(x + x) let c: W = m >>u 1
    }
  • (p + p) & 30 → (p & 15) << 1

or_shl_const — rule

An or or xor moves too when it sets no bit the shift clears.

rule or_shl_const<W>(x: W, k: const W, m: const W) {
        (x << k) | m => (x | c) << k if (m & ~(ones << k)) == 0 && one_use(x << k) let c: W = m >>u k
    }
  • (p << 2) | 12 → (p | 3) << 2

xor_shl_const — rule

rule xor_shl_const<W>(x: W, k: const W, m: const W) {
        (x << k) ^ m => (x ^ c) << k if (m & ~(ones << k)) == 0 && one_use(x << k) let c: W = m >>u k
    }
  • (p << 2) ^ 12 → (p ^ 3) << 2

or_double_const — rule

rule or_double_const<W>(x: W, m: const W) {
        (x + x) | m => (x | c) << 1 if (m & 1) == 0 && one_use(x + x) let c: W = m >>u 1
    }
  • (p + p) | 6 → (p | 3) << 1

xor_double_const — rule

rule xor_double_const<W>(x: W, m: const W) {
        (x + x) ^ m => (x ^ c) << 1 if (m & 1) == 0 && one_use(x + x) let c: W = m >>u 1
    }
  • (p + p) ^ 6 → (p ^ 3) << 1

core.factor

shl_and — rule

A left shift distributes over &, so the shift of the conjunction is one shift.

rule shl_and<W>(x: W, y: W, s: W) { (x << s) & (y << s) => (x & y) << s }
  • (p << 3) & (q << 3) → (p & q) << 3

shl_or — rule

Likewise over |.

rule shl_or<W>(x: W, y: W, s: W) { (x << s) | (y << s) => (x | y) << s }
  • (p << 3) | (q << 3) → (p | q) << 3

shl_xor — rule

Likewise over ^.

rule shl_xor<W>(x: W, y: W, s: W) { (x << s) ^ (y << s) => (x ^ y) << s }
  • (p << 3) ^ (q << 3) → (p ^ q) << 3

shl_add — rule

A left shift multiplies by a power of two (or by 0), which distributes over +.

rule shl_add<W>(x: W, y: W, s: W) { (x << s) + (y << s) => (x + y) << s }
  • (p << 3) + (q << 3) → (p + q) << 3

shl_sub — rule

And over -.

rule shl_sub<W>(x: W, y: W, s: W) { (x << s) - (y << s) => (x - y) << s }
  • (p << 3) - (q << 3) → (p - q) << 3

lshr_and — rule

A logical right shift moves every bit alike, so it distributes over &.

rule lshr_and<W>(x: W, y: W, s: W) { (x >>u s) & (y >>u s) => (x & y) >>u s }
  • (p >>u 3) & (q >>u 3) → (p & q) >>u 3

lshr_or — rule

Likewise over |.

rule lshr_or<W>(x: W, y: W, s: W) { (x >>u s) | (y >>u s) => (x | y) >>u s }
  • (p >>u 3) | (q >>u 3) → (p | q) >>u 3

lshr_xor — rule

Likewise over ^.

rule lshr_xor<W>(x: W, y: W, s: W) { (x >>u s) ^ (y >>u s) => (x ^ y) >>u s }
  • (p >>u 3) ^ (q >>u 3) → (p ^ q) >>u 3

ashr_and — rule

An arithmetic right shift copies the sign bit, a bit like the others, so it distributes over &.

rule ashr_and<W>(x: W, y: W, s: W) { (x >>s s) & (y >>s s) => (x & y) >>s s }
  • (p >>s 3) & (q >>s 3) → (p & q) >>s 3

ashr_or — rule

Likewise over |.

rule ashr_or<W>(x: W, y: W, s: W) { (x >>s s) | (y >>s s) => (x | y) >>s s }
  • (p >>s 3) | (q >>s 3) → (p | q) >>s 3

ashr_xor — rule

Likewise over ^.

rule ashr_xor<W>(x: W, y: W, s: W) { (x >>s s) ^ (y >>s s) => (x ^ y) >>s s }
  • (p >>s 3) ^ (q >>s 3) → (p ^ q) >>s 3

not_ashr_not — rule

An arithmetic right shift commutes with complement.

rule not_ashr_not<W>(x: W, s: W) { ~(~x >>s s) => x >>s s }
  • ~(~p >>s 3) → p >>s 3

rotl_and — rule

A rotation moves every bit alike, so it distributes over &.

rule rotl_and<W>(x: W, y: W, s: W) { rotl(x, s) & rotl(y, s) => rotl(x & y, s) }
  • rotl(p, 3) & rotl(q, 3) → rotl(p & q, 3)

rotl_or — rule

Likewise over |.

rule rotl_or<W>(x: W, y: W, s: W) { rotl(x, s) | rotl(y, s) => rotl(x | y, s) }
  • rotl(p, 3) | rotl(q, 3) → rotl(p | q, 3)

rotl_xor — rule

Likewise over ^.

rule rotl_xor<W>(x: W, y: W, s: W) { rotl(x, s) ^ rotl(y, s) => rotl(x ^ y, s) }
  • rotl(p, 3) ^ rotl(q, 3) → rotl(p ^ q, 3)

not_rotl_not — rule

A rotation commutes with complement.

rule not_rotl_not<W>(x: W, s: W) { ~rotl(~x, s) => rotl(x, s) }
  • ~rotl(~p, 3) → rotl(p, 3)

zext_and — rule

Zero extension distributes over &.

rule zext_and<W, U>(x: W, y: W) where W < U { zext<U>(x) & zext<U>(y) => zext<U>(x & y) }
  • zext<16>(p) & zext<16>(q) → zext<16>(p & q)

zext_or — rule

Likewise over |.

rule zext_or<W, U>(x: W, y: W) where W < U { zext<U>(x) | zext<U>(y) => zext<U>(x | y) }
  • zext<16>(p) | zext<16>(q) → zext<16>(p | q)

zext_xor — rule

Likewise over ^.

rule zext_xor<W, U>(x: W, y: W) where W < U { zext<U>(x) ^ zext<U>(y) => zext<U>(x ^ y) }
  • zext<16>(p) ^ zext<16>(q) → zext<16>(p ^ q)

sext_and — rule

Sign extension copies the top bit, a bit like the others: it distributes over &.

rule sext_and<W, U>(x: W, y: W) where W < U { sext<U>(x) & sext<U>(y) => sext<U>(x & y) }
  • sext<16>(p) & sext<16>(q) → sext<16>(p & q)

sext_or — rule

Likewise over |.

rule sext_or<W, U>(x: W, y: W) where W < U { sext<U>(x) | sext<U>(y) => sext<U>(x | y) }
  • sext<16>(p) | sext<16>(q) → sext<16>(p | q)

sext_xor — rule

Likewise over ^.

rule sext_xor<W, U>(x: W, y: W) where W < U { sext<U>(x) ^ sext<U>(y) => sext<U>(x ^ y) }
  • sext<16>(p) ^ sext<16>(q) → sext<16>(p ^ q)

extract_and — rule

A slice of each operand of & is the slice of the conjunction.

rule extract_and<W, N, L>(x: W, y: W) where N + L <= W, N < W {
        extract<L, N>(x) & extract<L, N>(y) => extract<L, N>(x & y)
    }
  • extract<4, 4>(p) & extract<4, 4>(q) → extract<4, 4>(p & q)

extract_or — rule

Likewise for |.

rule extract_or<W, N, L>(x: W, y: W) where N + L <= W, N < W {
        extract<L, N>(x) | extract<L, N>(y) => extract<L, N>(x | y)
    }
  • extract<4, 4>(p) | extract<4, 4>(q) → extract<4, 4>(p | q)

extract_xor — rule

Likewise for ^.

rule extract_xor<W, N, L>(x: W, y: W) where N + L <= W, N < W {
        extract<L, N>(x) ^ extract<L, N>(y) => extract<L, N>(x ^ y)
    }
  • extract<4, 4>(p) ^ extract<4, 4>(q) → extract<4, 4>(p ^ q)

trunc_and — rule

The low bits of each operand of & are the low bits of the conjunction.

rule trunc_and<W, N>(x: W, y: W) where N < W { trunc<N>(x) & trunc<N>(y) => trunc<N>(x & y) }
  • trunc<4>(p) & trunc<4>(q) → trunc<4>(p & q)

trunc_or — rule

Likewise for |.

rule trunc_or<W, N>(x: W, y: W) where N < W { trunc<N>(x) | trunc<N>(y) => trunc<N>(x | y) }
  • trunc<4>(p) | trunc<4>(q) → trunc<4>(p | q)

trunc_xor — rule

Likewise for ^.

rule trunc_xor<W, N>(x: W, y: W) where N < W { trunc<N>(x) ^ trunc<N>(y) => trunc<N>(x ^ y) }
  • trunc<4>(p) ^ trunc<4>(q) → trunc<4>(p ^ q)

core.select_idioms

select_add — rule

Both arms add the same term.

rule select_add<W>(c: 1, x: W, y: W, z: W) { select(c, x + z, y + z) => select(c, x, y) + z }
  • select(p <u q, p + r, q + r) → select(p <u q, p, q) + r

select_xor — rule

Both arms xor the same term.

rule select_xor<W>(c: 1, x: W, y: W, z: W) { select(c, x ^ z, y ^ z) => select(c, x, y) ^ z }
  • select(p <u q, p ^ r, q ^ r) → select(p <u q, p, q) ^ r

select_swapped_add — rule

Swapped arms under one condition, combined commutatively: the minimum plus the maximum is the sum.

rule select_swapped_add<W>(c: 1, x: W, y: W) { select(c, x, y) + select(c, y, x) => x + y }
  • select(p <u q, p, q) + select(p <u q, q, p) → p + q

select_swapped_mul — rule

Likewise for *.

rule select_swapped_mul<W>(c: 1, x: W, y: W) { select(c, x, y) * select(c, y, x) => x * y }
  • select(p <u q, p, q) * select(p <u q, q, p) → p * q

select_swapped_and — rule

Likewise for &.

rule select_swapped_and<W>(c: 1, x: W, y: W) { select(c, x, y) & select(c, y, x) => x & y }
  • select(p <u q, p, q) & select(p <u q, q, p) → p & q

select_swapped_or — rule

Likewise for |.

rule select_swapped_or<W>(c: 1, x: W, y: W) { select(c, x, y) | select(c, y, x) => x | y }
  • select(p <u q, p, q) | select(p <u q, q, p) → p | q

select_swapped_xor — rule

Likewise for ^.

rule select_swapped_xor<W>(c: 1, x: W, y: W) { select(c, x, y) ^ select(c, y, x) => x ^ y }
  • select(p <u q, p, q) ^ select(p <u q, q, p) → p ^ q

select_not — rule

Both arms complement.

rule select_not<W>(c: 1, x: W, y: W) { select(c, ~x, ~y) => ~select(c, x, y) }
  • select(p <u q, ~p, ~q) → ~select(p <u q, p, q)

select_then_same — rule

The same condition again in the chosen arm.

rule select_then_same<W>(c: 1, x: W, y: W, z: W) { select(c, select(c, x, y), z) => select(c, x, z) }
  • select(p <u q, select(p <u q, p, q), r) → select(p <u q, p, r)

select_else_same — rule

The same condition again in the other arm.

rule select_else_same<W>(c: 1, x: W, y: W, z: W) { select(c, x, select(c, y, z)) => select(c, x, z) }
  • select(p <u q, r, select(p <u q, p, q)) → select(p <u q, r, q)

select_eq_then — rule

Choosing x when it equals y, else y: always y (== is commutative, so this is also select(y == x, x, y)).

rule select_eq_then<W>(x: W, y: W) { select(x == y, x, y) => y }
  • select(p == q, p, q) → q
  • select(p == q, q, p) → p

select_eq_difference — rule

When x == y, x - y is 0.

rule select_eq_difference<W>(x: W, y: W) { select(x == y, x - y, 0) => 0 }
  • select(p == q, p - q, 0) → 0

select_eq_offset — rule

When x == c, x + d is c + d.

rule select_eq_offset<W>(x: W, c: const W, d: const W, e: const W) {
        select(x == c, x + d, e) => e if e == c + d
    }
  • select(p == 5, p + 1, 6) → 6

select_zero_sum — rule

When x == 0, x + y is y.

rule select_zero_sum<W>(x: W, y: W) { select(x == 0, x + y, y) => y }
  • select(p == 0, p + q, q) → q

select_by_mask — rule

Choosing by a mask: m all ones takes x, all zeros y.

rule select_by_mask<W>(c: 1, x: W, y: W) where 1 < W {
        (x & sext<W>(c)) | (y & ~sext<W>(c)) => select(c, x, y)
    }
  • (p & sext<8>(r:1)) | (q & ~sext<8>(r:1)) → select(r:1, p, q)

select_by_xor_mask — rule

Choosing by a mask with xor: y ^ ((x ^ y) & m).

rule select_by_xor_mask<W>(c: 1, x: W, y: W) where 1 < W {
        ((x ^ y) & sext<W>(c)) ^ y => select(c, x, y)
    }
  • q ^ ((p ^ q) & sext<8>(r:1)) → select(r:1, p, q)

select_xor_zero — rule

The xor-mask choice once the mask became a select.

rule select_xor_zero<W>(c: 1, x: W, y: W) { select(c, x ^ y, 0) ^ y => select(c, x, y) }
  • select(r:1, p ^ q, 0) ^ q → select(r:1, p, q)

mask_by_decremented_bit — rule

zext(c) - 1 is all ones when c is false and zero when it is true: a mask. (With the rules that make x & -zext(c) a select and merge complementary selects, the branchless choice (x & -zext(c)) | (y & (zext(c) - 1)) becomes select(c, x, y).)

rule mask_by_decremented_bit<W>(c: 1, y: W) where 1 < W {
        (zext<W>(c) + ones) & y => select(c, 0, y)
    }
  • (zext<8>(r:1) - 1) & q → select(r:1, 0, q)

core.division

urem_self — rule

x divided by itself leaves no remainder (and x mod 0 is x, which is 0 then too).

rule urem_self<W>(x: W) { urem(x, x) => 0 }
  • urem(p, p) → 0

srem_self — rule

Likewise signed.

rule srem_self<W>(x: W) { srem(x, x) => 0 }
  • srem(p, p) → 0

urem_urem — rule

A remainder is already reduced.

rule urem_urem<W>(x: W, y: W) { urem(urem(x, y), y) => urem(x, y) }
  • urem(urem(p, q), q) → urem(p, q)

udiv_identity — rule

Division identity: quotient times divisor plus remainder (a zero divisor included: its quotient is all ones, times 0, and its remainder is x).

rule udiv_identity<W>(x: W, y: W) { udiv(x, y) * y + urem(x, y) => x }
  • udiv(p, q) * q + urem(p, q) → p

sdiv_identity — rule

Likewise signed (for 0: a quotient of ±1 times 0, and the remainder x).

rule sdiv_identity<W>(x: W, y: W) { sdiv(x, y) * y + srem(x, y) => x }
  • sdiv(p, q) * q + srem(p, q) → p

sdiv_pow2_identity — rule

The same by a power of two, whose product mul_pow2 makes a shift.

rule sdiv_pow2_identity<W>(x: W, c: const W, k: const W) {
        (sdiv(x, c) << k) + srem(x, c) => x if is_pow2(c) && k == ctz(c)
    }
  • (sdiv(p, 8) << 3) + srem(p, 8) → p

sdiv_nonnegative_pow2 — rule

Signed division of a value the facts show nonnegative by a positive power of two is a logical right shift.

rule sdiv_nonnegative_pow2<W>(x: W, c: const W) {
        sdiv(x, c) => x >>u k if is_pow2(c) && 0 <s c && proves(0 <=s x) let k: W = ctz(c)
    }
  • sdiv(p >>u 1, 8) → p >>u 1 >>u 3

core.sign_tests

zext_negative — rule

The sign bit of x as a word: x >>u (W - 1).

rule zext_negative<W>(x: W) where 1 < W { zext<W>(x <s 0) => x >>u (W - 1) }
  • zext<8>(p <s 0) → p >>u 7

sext_negative — rule

The sign mask of x: x >>s (W - 1).

rule sext_negative<W>(x: W) where 1 < W { sext<W>(x <s 0) => x >>s (W - 1) }
  • sext<8>(p <s 0) → p >>s 7

sign_bit_clear — rule

The sign bit is clear: x is nonnegative.

rule sign_bit_clear<W>(x: W) where 1 < W { (x >>u (W - 1)) == 0 => 0 <=s x }
  • (p >>u 7) == 0 → 0 <=s p

sign_mask_zero — rule

The sign mask is 0: x is nonnegative.

rule sign_mask_zero<W>(x: W) where 1 < W { (x >>s (W - 1)) == 0 => 0 <=s x }
  • (p >>s 7) == 0 → 0 <=s p

sign_mask_ones — rule

The sign mask is all ones: x is negative.

rule sign_mask_ones<W>(x: W) where 1 < W { (x >>s (W - 1)) == ones => x <s 0 }
  • (p >>s 7) == 0xff → p <s 0

sign_by_mask — rule

The sign bit tested through a mask.

rule sign_by_mask<W>(x: W) { (x & smin_lit) != 0 => x <s 0 }
  • (p & 0x80) != 0 → p <s 0

sign_clear_by_mask — rule

The sign bit clear, tested through a mask.

rule sign_clear_by_mask<W>(x: W) { (x & smin_lit) == 0 => 0 <=s x }
  • (p & 0x80) == 0 → 0 <=s p

sign_mask_and_bit — rule

The sign mask’s low bit is the sign bit.

rule sign_mask_and_bit<W>(x: W) where 1 < W { (x >>s (W - 1)) & (x >>u (W - 1)) => x >>u (W - 1) }
  • (p >>s 7) & (p >>u 7) → p >>u 7

neg_sign_mask — rule

The negated sign mask is the sign bit.

rule neg_sign_mask<W>(x: W) where 1 < W { -(x >>s (W - 1)) => x >>u (W - 1) }
  • -(p >>s 7) → p >>u 7

ult_sign_flipped — rule

Flipping both sign bits turns an unsigned comparison into a signed one.

rule ult_sign_flipped<W>(x: W, y: W) { (x ^ smin_lit) <u (y ^ smin_lit) => x <s y }
  • (p ^ 0x80) <u (q ^ 0x80) → p <s q

slt_sign_flipped — rule

And a signed one into an unsigned one.

rule slt_sign_flipped<W>(x: W, y: W) { (x ^ smin_lit) <s (y ^ smin_lit) => x <u y }
  • (p ^ 0x80) <s (q ^ 0x80) → p <u q

ult_offset_by_smin — rule

Adding the sign bit flips it (the builder’s form of x + smin).

rule ult_offset_by_smin<W>(x: W, y: W) { (x + smin_lit) <u (y + smin_lit) => x <s y }
  • (p + 0x80) <u (q + 0x80) → p <s q

slt_offset_by_smin — rule

Likewise signed.

rule slt_offset_by_smin<W>(x: W, y: W) { (x + smin_lit) <s (y + smin_lit) => x <u y }
  • (p + 0x80) <s (q + 0x80) → p <u q

slt_zext — rule

Zero-extended values are nonnegative, so their signed order is the unsigned one.

rule slt_zext<W, U>(x: W, y: W) where W < U { zext<U>(x) <s zext<U>(y) => x <u y }
  • zext<16>(p) <s zext<16>(q) → p <u q

core.bounds

and_at_most — rule

A conjunction is at most each operand (so is x & -x, its lowest set bit).

rule and_at_most<W>(x: W, y: W) { (x & y) <=u x => true }
  • (p & q) <=u p → true
  • (p & -p) <=u p → true

and_not_above — rule

No operand is below its conjunction with anything.

rule and_not_above<W>(x: W, y: W) { x <u (x & y) => false }
  • p <u (p & q) → false

or_at_least — rule

A disjunction is at least each operand.

rule or_at_least<W>(x: W, y: W) { x <=u (x | y) => true }
  • p <=u (p | q) → true

or_not_below — rule

No disjunction is below one of its operands.

rule or_not_below<W>(x: W, y: W) { (x | y) <u x => false }
  • (p | q) <u p → false

and_at_most_or — rule

A conjunction is at most the disjunction.

rule and_at_most_or<W>(x: W, y: W) { (x & y) <=u (x | y) => true }
  • (p & q) <=u (p | q) → true

lshr_at_most — rule

A logical right shift does not grow a value.

rule lshr_at_most<W>(x: W, s: W) { (x >>u s) <=u x => true }
  • (p >>u 1) <=u p → true

udiv_at_most — rule

Nor does unsigned division (by 0 it is all ones… except that x / 0 is all ones, so only by a divisor the facts show nonzero).

rule udiv_at_most<W>(x: W, y: W) { udiv(x, y) <=u x => true if proves(y != 0) }
  • udiv(p, q | 1) <=u p → true

urem_at_most — rule

A remainder is at most the dividend (by 0 it is the dividend).

rule urem_at_most<W>(x: W, y: W) { urem(x, y) <=u x => true }
  • urem(p, q) <=u p → true

urem_below_divisor — rule

A remainder is below its divisor, or (for 0) at most y - 1, all ones.

rule urem_below_divisor<W>(x: W, y: W) { urem(x, y) <=u (y + ones) => true }
  • urem(p, q) <=u q - 1 → true

decrement_below — rule

x - 1 < x except at 0, where it wraps.

rule decrement_below<W>(x: W) { (x + ones) <u x => x != 0 }
  • p - 1 <u p → p != 0

increment_wraps — rule

x + 1 < x only at all ones, where it wraps.

rule increment_wraps<W>(x: W) { (x + 1) <u x => x == ones }
  • p + 1 <u p → p == 0xff

signed_increment_wraps — rule

x + 1 <s x only at the signed maximum.

rule signed_increment_wraps<W>(x: W) { (x + 1) <s x => x == smax_lit }
  • p + 1 <s p → p == 0x7f

signed_increment_grows — rule

x <s x + 1 except at the signed maximum.

rule signed_increment_grows<W>(x: W) { x <s (x + 1) => x != smax_lit }
  • p <s p + 1 → p != 0x7f

longer_shift_smaller — rule

The larger of two shifts right by constants leaves the smaller value.

rule longer_shift_smaller<W>(x: W, a: const W, b: const W) {
        (x >>u a) <=u (x >>u b) => true if b <=u a
    }
  • (p >>u 3) <=u (p >>u 1) → true

halves_sum_no_overflow — rule

Halves of two values never overflow their sum.

rule halves_sum_no_overflow<W>(x: W, y: W) where 1 < W {
        ((x >>u 1) + (y >>u 1)) <u (x >>u 1) => false
    }
  • ((p >>u 1) + (q >>u 1)) <u (p >>u 1) → false

core.concat

concat_and_low — rule

& of concatenations with one low part: the low part stays.

rule concat_and_low<W, V>(x: W, y: V, z: W) { concat(x, y) & concat(z, y) => concat(x & z, y) }
  • concat(p, r:4) & concat(q, r:4) → concat(p & q, r:4)

concat_and_high — rule

Likewise with one high part.

rule concat_and_high<W, V>(x: W, y: V, z: V) { concat(x, y) & concat(x, z) => concat(x, y & z) }
  • concat(r:4, p) & concat(r:4, q) → concat(r:4, p & q)

concat_or_low — rule

| of concatenations with one low part.

rule concat_or_low<W, V>(x: W, y: V, z: W) { concat(x, y) | concat(z, y) => concat(x | z, y) }
  • concat(p, r:4) | concat(q, r:4) → concat(p | q, r:4)

concat_or_high — rule

Likewise with one high part.

rule concat_or_high<W, V>(x: W, y: V, z: V) { concat(x, y) | concat(x, z) => concat(x, y | z) }
  • concat(r:4, p) | concat(r:4, q) → concat(r:4, p | q)

concat_xor_low — rule

^ of concatenations with one low part: that part cancels.

rule concat_xor_low<W, V>(x: W, y: V, z: W) { concat(x, y) ^ concat(z, y) => concat(x ^ z, zero) }
  • concat(p, r:4) ^ concat(q, r:4) → concat(p ^ q, 0:4)

concat_assoc — rule

Concatenation associates; right nesting is the canonical one.

rule concat_assoc<A, B, C>(x: A, y: B, z: C) { concat(concat(x, y), z) => concat(x, concat(y, z)) }
  • concat(concat(p, q), r) → concat(p, concat(q, r))

concat_ashr — rule

An arithmetic shift right by the low part’s width sign-extends the high part.

rule concat_ashr<W, V>(x: W, y: V) { concat(x, y) >>s V => sext<W + V>(x) }
  • concat(p, q:4) >>s 4 → sext<12>(p)

concat_lshr — rule

A logical shift right by the low part’s width zero-extends the high part.

rule concat_lshr<W, V>(x: W, y: V) { concat(x, y) >>u V => zext<W + V>(x) }
  • concat(p, q:4) >>u 4 → zext<12>(p)

concat_sign_word — rule

A value above a sign-extended copy of its top bit is its sign extension.

rule concat_sign_word<W>(x: W) where 1 < W {
        concat(sext<W>(extract<W - 1, 1>(x)), x) => sext<W + W>(x)
    }
  • concat(sext<8>(extract<7, 1>(p)), p) → sext<16>(p)

concat_or_zext — rule

A zero low part with a zero-extended value in it is their concatenation (a sum of the two has no carries, so it is this | too).

rule concat_or_zext<W, V>(x: W, y: V) { concat(x, 0) | zext<W + V>(y) => concat(x, y) }
  • concat(p, 0:4) | zext<12>(q:4) → concat(p, q:4)

core.single_bit

pow2_and_below — rule

A power of two shares no bit with the value one below it (and 1 << s past the width is 0).

rule pow2_and_below<W>(s: W) { (one << s) & ((one << s) + ones) => 0 }
  • (1 << (q & 7)) & ((1 << (q & 7)) - 1) → 0

core.float

div_pow2 — rule

Dividing by a power of two is multiplying by its reciprocal when that is a normal number: the two are the same real number, rounded once in the same mode, so every result (a NaN, an infinity, the sign of a zero, an underflow) is the same. Not by ½: the builder already writes x / ½ (and x · 2) as x + x.

rule div_pow2<E, S>(x: E + S, c: const E + S, r: rm) {
        fp.div.r<E, S>(x, c) => fp.mul.r<E, S>(x, k)
        if (c & lowmask(S - 1)) == 0 && (c & smax_lit) != 0 && (c & smax_lit) <u fp.inf<E, S>
            && m >=u fp.min_normal<E, S> && m != fp.two<E, S>
        let m: E + S = fp.one<E, S> + fp.one<E, S> - (c & smax_lit)
        let k: E + S = (c & smin_lit) | m
    }
  • fp.div.rne.f32(p:32, 0x40800000) → fp.mul.rne.f32(p:32, 0x3e800000)

mul_one — rule

x · 1 is x for every number.

rule mul_one<E, S>(x: E + S, r: rm) {
        fp.mul.r<E, S>(x, fp.one<E, S>) => x if fp.not_nan<E, S>(x)
    }
  • fp.mul.rne.f64(fp.from_sbv.rne.f64(i:32), 0x3ff0000000000000) → fp.from_sbv.rne.f64(i:32)

mul_neg_one — rule

x · −1 is −x for every number.

rule mul_neg_one<E, S>(x: E + S, r: rm) {
        fp.mul.r<E, S>(x, fp.none<E, S>) => fp.neg<E, S>(x) if fp.not_nan<E, S>(x)
    }
  • fp.mul.rtz.f64(fp.from_sbv.rne.f64(i:32), 0xbff0000000000000) → fp.from_sbv.rne.f64(i:32) ^ 0x8000000000000000

add_zero — rule

Adding +0 to a nonzero number changes nothing, in every mode.

rule add_zero<E, S>(x: E + S, r: rm) {
        fp.add.r<E, S>(x, fp.zero<E, S>) => x if fp.not_nan<E, S>(x) && fp.nonzero<E, S>(x)
    }
  • fp.add.rtp.f32(fp.from_ubv.rne.f32(i:8 | 1), 0) → fp.from_ubv.rne.f32(i:8 | 1)

add_neg_zero — rule

Adding −0 to a nonzero number changes nothing, in every mode.

rule add_neg_zero<E, S>(x: E + S, r: rm) {
        fp.add.r<E, S>(x, fp.nzero<E, S>) => x if fp.not_nan<E, S>(x) && fp.nonzero<E, S>(x)
    }
  • fp.add.rtn.f32(fp.from_ubv.rne.f32(i:8 | 1), 0x80000000) → fp.from_ubv.rne.f32(i:8 | 1)

add_neg_zero_rne — rule

To nearest even, adding −0 changes no number, a zero included (−0 + −0 is −0, +0 + −0 is +0): x − 0 is x.

rule add_neg_zero_rne<E, S>(x: E + S) {
        fp.add.rne<E, S>(x, fp.nzero<E, S>) => x if fp.not_nan<E, S>(x)
    }
  • fp.sub.rne.f64(fp.from_sbv.rne.f64(i:32), 0) → fp.from_sbv.rne.f64(i:32)

eq_self — rule

A number equals itself (a NaN does not).

rule eq_self<E, S>(x: E + S) { fp.eq<E, S>(x, x) => true if fp.not_nan<E, S>(x) }
  • fp.eq.f64(fp.from_sbv.rne.f64(i:32), fp.from_sbv.rne.f64(i:32)) → true

le_self — rule

A number is at most itself.

rule le_self<E, S>(x: E + S) { fp.le<E, S>(x, x) => true if fp.not_nan<E, S>(x) }
  • fp.le.f64(fp.from_sbv.rne.f64(i:32), fp.from_sbv.rne.f64(i:32)) → true

min_self — rule

A number is its own minimum and maximum.

rule min_self<E, S>(x: E + S) { fp.min<E, S>(x, x) => x if fp.not_nan<E, S>(x) }
  • fp.min.f64(fp.from_sbv.rne.f64(i:32), fp.from_sbv.rne.f64(i:32)) → fp.from_sbv.rne.f64(i:32)

max_self — rule

rule max_self<E, S>(x: E + S) { fp.max<E, S>(x, x) => x if fp.not_nan<E, S>(x) }
  • fp.max.f64(fp.from_sbv.rne.f64(i:32), fp.from_sbv.rne.f64(i:32)) → fp.from_sbv.rne.f64(i:32)

div_self — rule

A finite nonzero number divided by itself is exactly 1, in every mode.

rule div_self<E, S>(x: E + S, r: rm) {
        fp.div.r<E, S>(x, x) => fp.one<E, S> if fp.finite<E, S>(x) && fp.nonzero<E, S>(x)
    }
  • fp.div.rne.f64(fp.from_ubv.rne.f64(zext<32>(u:8) + 1), fp.from_ubv.rne.f64(zext<32>(u:8) + 1)) → 0x3ff0000000000000:64

sub_self_rne — rule

A finite number minus itself is +0 (the sum of opposites rounds to +0 except toward −∞).

rule sub_self_rne<E, S>(x: E + S) { fp.add.rne<E, S>(x, fp.neg<E, S>(x)) => fp.zero<E, S> if fp.finite<E, S>(x) }
  • fp.sub.rne.f64(fp.from_sbv.rne.f64(i:32), fp.from_sbv.rne.f64(i:32)) → 0:64

sub_self_rna — rule

Likewise to nearest, ties away.

rule sub_self_rna<E, S>(x: E + S) { fp.add.rna<E, S>(x, fp.neg<E, S>(x)) => fp.zero<E, S> if fp.finite<E, S>(x) }
  • fp.sub.rna.f64(fp.from_sbv.rne.f64(i:32), fp.from_sbv.rne.f64(i:32)) → 0:64

sub_self_rtp — rule

Likewise toward +∞.

rule sub_self_rtp<E, S>(x: E + S) { fp.add.rtp<E, S>(x, fp.neg<E, S>(x)) => fp.zero<E, S> if fp.finite<E, S>(x) }
  • fp.sub.rtp.f64(fp.from_sbv.rne.f64(i:32), fp.from_sbv.rne.f64(i:32)) → 0:64

sub_self_rtz — rule

Likewise toward 0.

rule sub_self_rtz<E, S>(x: E + S) { fp.add.rtz<E, S>(x, fp.neg<E, S>(x)) => fp.zero<E, S> if fp.finite<E, S>(x) }
  • fp.sub.rtz.f64(fp.from_sbv.rne.f64(i:32), fp.from_sbv.rne.f64(i:32)) → 0:64

sub_self_rtn — rule

Toward −∞, a finite number minus itself is −0.

rule sub_self_rtn<E, S>(x: E + S) { fp.add.rtn<E, S>(x, fp.neg<E, S>(x)) => fp.nzero<E, S> if fp.finite<E, S>(x) }
  • fp.sub.rtn.f64(fp.from_sbv.rne.f64(i:32), fp.from_sbv.rne.f64(i:32)) → 0x8000000000000000:64

sqrt_square — rule

The square root of a square is the magnitude when the square is a normal number (no overflow, no underflow), to nearest: |x| between 2^⌈emin/2⌉ and 2^⌊emax/2⌋.

rule sqrt_square<E, S>(x: E + S) {
        fp.sqrt.rne<E, S>(fp.mul.rne<E, S>(x, x)) => fp.abs<E, S>(x)
        if (proves(lo <=u x) && proves(x <u hi)) || (proves(nlo <=u x) && proves(x <u nhi))
        let b: E + S = (one << (E - 1)) - one
        let lo: E + S = (b - ((b - one) >>u 1)) << (S - 1)
        let hi: E + S = (b + (b >>u 1)) << (S - 1)
        let nlo: E + S = lo | smin_lit
        let nhi: E + S = hi | smin_lit
    }
  • fp.sqrt.rne.f64(fp.mul.rne.f64(fp.from_ubv.rne.f64(zext<32>(u:8) + 1), fp.from_ubv.rne.f64(zext<32>(u:8) + 1))) → fp.from_ubv.rne.f64(zext<32>(u:8) + 1) & 0x7fffffffffffffff

widen_narrow — rule

Widening and narrowing back is exact for numbers (every number of the narrower format is one of the wider).

rule widen_narrow<E, S, F, T>(x: E + S, r: rm, q: rm) where E <= F, S <= T {
        fp.convert.r<F, T, E, S>(fp.convert.q<E, S, F, T>(x)) => x if fp.not_nan<E, S>(x)
    }
  • fp.convert.rne.f64.f32(fp.convert.rne.f32.f64(fp.from_sbv.rne.f32(i:16))) → fp.from_sbv.rne.f32(i:16)

core.float_values

mul_one — rule (#[float_values]: equal as floats, applied only on request)

x · 1 is x as a float (a NaN’s product is the canonical NaN). With div_pow2, x / 1 is x too.

rule mul_one<E, S>(x: E + S, r: rm) { fp.mul.r<E, S>(x, fp.one<E, S>) => x }
  • fp.mul.rne.f32(p:32, 0x3f800000) → p:32

mul_neg_one — rule (#[float_values]: equal as floats, applied only on request)

x · −1 is −x as a float.

rule mul_neg_one<E, S>(x: E + S, r: rm) { fp.mul.r<E, S>(x, fp.none<E, S>) => fp.neg<E, S>(x) }
  • fp.mul.rne.f32(p:32, 0xbf800000) → p:32 ^ 0x80000000

add_neg_zero_rne — rule (#[float_values]: equal as floats, applied only on request)

x + (−0) is x as a float, to nearest (toward −∞, +0 + −0 is −0).

rule add_neg_zero_rne<E, S>(x: E + S) { fp.add.rne<E, S>(x, fp.nzero<E, S>) => x }
  • fp.add.rne.f32(p:32, 0x80000000) → p:32

add_neg_zero_rna — rule (#[float_values]: equal as floats, applied only on request)

Likewise to nearest, ties away.

rule add_neg_zero_rna<E, S>(x: E + S) { fp.add.rna<E, S>(x, fp.nzero<E, S>) => x }
  • fp.add.rna.f32(p:32, 0x80000000) → p:32

add_neg_zero_rtp — rule (#[float_values]: equal as floats, applied only on request)

Likewise toward +∞.

rule add_neg_zero_rtp<E, S>(x: E + S) { fp.add.rtp<E, S>(x, fp.nzero<E, S>) => x }
  • fp.add.rtp.f32(p:32, 0x80000000) → p:32

add_neg_zero_rtz — rule (#[float_values]: equal as floats, applied only on request)

Likewise toward 0.

rule add_neg_zero_rtz<E, S>(x: E + S) { fp.add.rtz<E, S>(x, fp.nzero<E, S>) => x }
  • fp.add.rtz.f32(p:32, 0x80000000) → p:32

min_self — rule (#[float_values]: equal as floats, applied only on request)

A value is its own minimum.

rule min_self<E, S>(x: E + S) { fp.min<E, S>(x, x) => x }
  • fp.min.f32(p:32, p:32) → p:32

max_self — rule (#[float_values]: equal as floats, applied only on request)

And its own maximum.

rule max_self<E, S>(x: E + S) { fp.max<E, S>(x, x) => x }
  • fp.max.f32(p:32, p:32) → p:32

widen_narrow — rule (#[float_values]: equal as floats, applied only on request)

Widening and narrowing back is x as a float.

rule widen_narrow<E, S, F, T>(x: E + S, r: rm, q: rm) where E <= F, S <= T {
        fp.convert.r<F, T, E, S>(fp.convert.q<E, S, F, T>(x)) => x
    }
  • fp.convert.rne.f64.f32(fp.convert.rne.f32.f64(p:32)) → p:32

eqsat.bwr

eqsat.assoc

add_assoc — identity (search only)

identity add_assoc<W>(x: W, y: W, z: W) { (x + y) + z <=> x + (y + z) }
  • (p + q) + r → p + (q + r)

mul_assoc — identity (search only)

identity mul_assoc<W>(x: W, y: W, z: W) { (x * y) * z <=> x * (y * z) }
  • (p * q) * r → p * (q * r)

and_assoc — identity (search only)

identity and_assoc<W>(x: W, y: W, z: W) { (x & y) & z <=> x & (y & z) }
  • (p & q) & r → p & (q & r)

or_assoc — identity (search only)

identity or_assoc<W>(x: W, y: W, z: W) { (x | y) | z <=> x | (y | z) }
  • (p | q) | r → p | (q | r)

xor_assoc — identity (search only)

identity xor_assoc<W>(x: W, y: W, z: W) { (x ^ y) ^ z <=> x ^ (y ^ z) }
  • (p ^ q) ^ r → p ^ (q ^ r)

eqsat.distrib

mul_add — identity (search only)

identity mul_add<W>(x: W, y: W, z: W) { x * (y + z) <=> x * y + x * z }
  • p * (q + r) → p * q + p * r

eqsat.distrib_and

and_or — identity (search only)

identity and_or<W>(x: W, y: W, z: W) { x & (y | z) <=> (x & y) | (x & z) }
  • p & (q | r) → (p & q) | (p & r)

and_xor — identity (search only)

identity and_xor<W>(x: W, y: W, z: W) { x & (y ^ z) <=> (x & y) ^ (x & z) }
  • p & (q ^ r) → (p & q) ^ (p & r)

eqsat.distrib_or

or_and — identity (search only)

identity or_and<W>(x: W, y: W, z: W) { x | (y & z) <=> (x | y) & (x | z) }
  • p | (q & r) → (p | q) & (p | r)

eqsat.negation

de_morgan_and — identity (search only)

identity de_morgan_and<W>(x: W, y: W) { ~(x & y) <=> ~x | ~y }
  • ~(p & q) → ~p | ~q

de_morgan_or — identity (search only)

identity de_morgan_or<W>(x: W, y: W) { ~(x | y) <=> ~x & ~y }
  • ~(p | q) → ~p & ~q

neg_add — identity (search only)

identity neg_add<W>(x: W, y: W) { -(x + y) <=> -x + -y }
  • -(p + q) → -p + -q

neg_mul — identity (search only)

identity neg_mul<W>(x: W, y: W) { -(x * y) <=> -x * y }
  • -(p * q) → -p * q
identity not_xor<W>(x: W, y: W) { ~(x ^ y) <=> ~x ^ y }
  • ~(p ^ q) → ~p ^ q

eqsat.cancel

add_sub — rule

rule add_sub<W>(x: W, y: W) { (x + y) - y => x }
  • (p + q) - q → p

sub_add — rule

rule sub_add<W>(x: W, y: W) { (x - y) + y => x }
  • (p - q) + q → p

xor_xor — rule

rule xor_xor<W>(x: W, y: W) { (x ^ y) ^ y => x }
  • (p ^ q) ^ q → p

neg_neg — rule

rule neg_neg<W>(x: W) { -(-x) => x }

not_not — rule

rule not_not<W>(x: W) { ~(~x) => x }

add_neg — rule

rule add_neg<W>(x: W) { x + -x => 0 }
  • p + -p → 0

mul_one — rule

rule mul_one<W>(x: W) { x * 1 => x }

mul_zero — rule

rule mul_zero<W>(x: W) { x * 0 => 0 }

add_zero — rule

rule add_zero<W>(x: W) { x + 0 => x }