Skip to main content

logicaffeine_compile/spatial/
wide_int.rs

1//! 64-bit integers on a chip whose integer lanes are 32 bits wide.
2//!
3//! An RDU computes integers in INT32 ([`super::rdu_chip::RduChipFacts::index_datatype`]), and some of what a model
4//! asks for needs 64 bits: Qwen3.8-Flash-Next's n-gram hash multiplies a token by a multiplier below 2^45 and reduces
5//! the exclusive-or of such products by a prime ([`crate::model::ngram_hash`]), and an index past 2^31 needs a wider
6//! type. [`lower`] turns each 64-bit operation ([`Op64`]) into a [`LaneProgram`]: straight-line 32-bit lane operations
7//! (wrapping add and subtract, the low half of a product, bitwise operations, shifts by a constant), a 64-bit operand
8//! travelling as its low and its high lane.
9//!
10//! The arithmetic lowerings keep every lane operation exact. An operand is cut into 16-bit limbs, so a limb product
11//! fits a lane; sums are carried limb by limb; and a value is only ever cut by a shift and a mask. A subtraction adds
12//! the complement plus one. The remainder by `d` is Barrett's: the quotient estimate `⌊x · m / 2^64⌋` with
13//! `m = ⌊2^64 / d⌋`, the difference `x − q·d`, which lies in `[0, 2d)`, and one conditional subtraction of `d`.
14//!
15//! [`prove`] proves each lowering equal to its 64-bit definition. The arithmetic ones by their algebra: the program is
16//! evaluated symbolically over the integers, with an interval for every lane that shows no operation wraps; every cut
17//! `v = h · 2^k + l` keeps `h` as a symbol and writes `l` as `v − h · 2^k`; and what the outputs then are is a
18//! polynomial identity — the result equals the definition plus `2^64` times an integer polynomial — that the kernel
19//! checks by ring normalization (the first kernel's `try_ring` reduced to `DRingSolve` and its conclusion, by
20//! `infer_type`). The second kernel re-checks each identity by exact evaluation at a Kronecker point where that fits its
21//! bound on slots (one per symbol at degree one: the add's and subtract's identities, not the multiply's); it never
22//! disagrees, and a proof records which identities it decided ([`KernelVerdict`]). The remainder's proof is a chain of
23//! such identities, the intervals that bound Barrett's difference to `[0, 2d)`, and the two cases of its correction. The bitwise lowerings and the shifts are wiring, proved by a certified equivalence
24//! check of their bit-blasted circuits ([`logicaffeine_proof::fraig`], its LRAT derivation replayed).
25
26use crate::codegen_sva::fabric_target::Precision;
27use crate::spatial::target::SpatialTarget;
28use logicaffeine_base::numeric::BigInt;
29use logicaffeine_kernel::prelude::StandardLibrary;
30use logicaffeine_kernel::tactic_cert::{syn_binary, syn_typed_eq, term_of_syn, CertifiablePrimitive, Syn};
31use logicaffeine_kernel::{double_check, infer_type, Context, DoubleCheck, Term};
32use logicaffeine_proof::aig::{Aig, AigLit};
33use logicaffeine_proof::fraig;
34use std::collections::BTreeMap;
35
36/// Whether a 64-bit integer needs limbs on `target`: its datapath stores no 64-bit integer.
37pub fn needs_limbs(target: &SpatialTarget) -> bool {
38    !target.storable_ints().contains(&Precision::Int64)
39}
40
41/// A 64-bit operation on unsigned integers; a shift carries its distance and a remainder its divisor.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43pub enum Op64 {
44    /// `a + b` modulo 2^64.
45    Add,
46    /// `a − b` modulo 2^64.
47    Sub,
48    /// The low 64 bits of `a · b`.
49    Mul,
50    And,
51    Or,
52    Xor,
53    /// `a << k`, `k < 64`.
54    Shl(u32),
55    /// `a >> k`, `k < 64`.
56    Shr(u32),
57    /// `a mod d`, `d ≥ 1`.
58    Rem(u32),
59}
60
61impl Op64 {
62    /// The 64-bit operands it takes.
63    pub fn arity(self) -> usize {
64        match self {
65            Op64::Shl(_) | Op64::Shr(_) | Op64::Rem(_) => 1,
66            _ => 2,
67        }
68    }
69
70    /// Its definition; a unary operation ignores `b`.
71    pub fn apply(self, a: u64, b: u64) -> u64 {
72        match self {
73            Op64::Add => a.wrapping_add(b),
74            Op64::Sub => a.wrapping_sub(b),
75            Op64::Mul => a.wrapping_mul(b),
76            Op64::And => a & b,
77            Op64::Or => a | b,
78            Op64::Xor => a ^ b,
79            Op64::Shl(k) => a << k,
80            Op64::Shr(k) => a >> k,
81            Op64::Rem(d) => a % u64::from(d),
82        }
83    }
84
85    fn check(self) {
86        match self {
87            Op64::Shl(k) | Op64::Shr(k) => assert!(k < 64, "a 64-bit shift by {k}"),
88            Op64::Rem(d) => assert!(d >= 1, "a remainder by zero"),
89            _ => {}
90        }
91    }
92}
93
94/// A register: the index of the operation that defines it.
95pub type Reg = usize;
96
97/// One operation of a 32-bit lane.
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub enum LaneOp {
100    /// The `i`-th input lane: a 64-bit operand's low lane, then its high lane, operand by operand.
101    Input(usize),
102    Const(u32),
103    /// Wrapping.
104    Add(Reg, Reg),
105    /// Wrapping.
106    Sub(Reg, Reg),
107    /// The low 32 bits of the product.
108    MulLo(Reg, Reg),
109    And(Reg, Reg),
110    Or(Reg, Reg),
111    Xor(Reg, Reg),
112    /// By a constant below 32.
113    Shl(Reg, u32),
114    /// Logical, by a constant below 32.
115    Shr(Reg, u32),
116}
117
118/// Where a remainder's program holds the values its proof speaks of.
119#[derive(Clone, Debug, PartialEq, Eq)]
120struct RemMarks {
121    /// The eight 16-bit limbs of the exact product `x · m`, least significant first.
122    product: Vec<Reg>,
123    /// The four limbs of `x − q·d` modulo 2^64.
124    diff: Vec<Reg>,
125    /// The four limbs of `diff + 2^34 − d` modulo 2^64.
126    probe: Vec<Reg>,
127    /// `probe >> 34`: one exactly when the difference is at least `d`.
128    over: Reg,
129}
130
131/// A straight-line program over 32-bit lanes computing one 64-bit operation: its inputs are each operand's low and
132/// high lane, its two outputs the result's.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct LaneProgram {
135    op: Op64,
136    ops: Vec<LaneOp>,
137    outputs: [Reg; 2],
138    rem: Option<RemMarks>,
139}
140
141impl LaneProgram {
142    pub fn op(&self) -> Op64 {
143        self.op
144    }
145
146    pub fn ops(&self) -> &[LaneOp] {
147        &self.ops
148    }
149
150    pub fn len(&self) -> usize {
151        self.ops.len()
152    }
153
154    pub fn is_empty(&self) -> bool {
155        self.ops.is_empty()
156    }
157
158    /// The input lanes: two for each operand.
159    pub fn inputs(&self) -> usize {
160        2 * self.op.arity()
161    }
162
163    /// The output lanes: the result's low and high.
164    pub fn outputs(&self) -> usize {
165        self.outputs.len()
166    }
167
168    /// Every register's value on `lanes`, as a 32-bit lane computes it.
169    pub fn eval(&self, lanes: &[u32]) -> Vec<u32> {
170        assert_eq!(lanes.len(), self.inputs(), "{:?} takes {} lanes", self.op, self.inputs());
171        let mut v: Vec<u32> = Vec::with_capacity(self.ops.len());
172        for op in &self.ops {
173            let x = match *op {
174                LaneOp::Input(i) => lanes[i],
175                LaneOp::Const(c) => c,
176                LaneOp::Add(a, b) => v[a].wrapping_add(v[b]),
177                LaneOp::Sub(a, b) => v[a].wrapping_sub(v[b]),
178                LaneOp::MulLo(a, b) => v[a].wrapping_mul(v[b]),
179                LaneOp::And(a, b) => v[a] & v[b],
180                LaneOp::Or(a, b) => v[a] | v[b],
181                LaneOp::Xor(a, b) => v[a] ^ v[b],
182                LaneOp::Shl(a, k) => v[a] << k,
183                LaneOp::Shr(a, k) => v[a] >> k,
184            };
185            v.push(x);
186        }
187        v
188    }
189
190    /// The program with operation `i` replaced: a variant to show that a proof is refused to a program that is not
191    /// the lowering.
192    pub fn with_op(&self, i: usize, op: LaneOp) -> LaneProgram {
193        let mut p = self.clone();
194        p.ops[i] = op;
195        p
196    }
197
198    /// The 64-bit result on 64-bit operands (`b` unused by a unary operation).
199    pub fn run(&self, a: u64, b: u64) -> u64 {
200        let mut lanes = vec![a as u32, (a >> 32) as u32];
201        if self.op.arity() == 2 {
202            lanes.extend([b as u32, (b >> 32) as u32]);
203        }
204        let v = self.eval(&lanes);
205        u64::from(v[self.outputs[0]]) | u64::from(v[self.outputs[1]]) << 32
206    }
207}
208
209/// A 16-bit limb operand: a register below 2^16, or a constant one.
210#[derive(Clone, Copy, Debug)]
211enum Limb {
212    Reg(Reg),
213    K(u32),
214}
215
216struct Builder {
217    ops: Vec<LaneOp>,
218    consts: BTreeMap<u32, Reg>,
219}
220
221const MASK16: u32 = 0xFFFF;
222
223impl Builder {
224    fn new() -> Builder {
225        Builder { ops: Vec::new(), consts: BTreeMap::new() }
226    }
227
228    fn push(&mut self, op: LaneOp) -> Reg {
229        self.ops.push(op);
230        self.ops.len() - 1
231    }
232
233    fn konst(&mut self, c: u32) -> Reg {
234        if let Some(&r) = self.consts.get(&c) {
235            return r;
236        }
237        let r = self.push(LaneOp::Const(c));
238        self.consts.insert(c, r);
239        r
240    }
241
242    fn lo16(&mut self, x: Reg) -> Reg {
243        let m = self.konst(MASK16);
244        self.push(LaneOp::And(x, m))
245    }
246
247    fn hi16(&mut self, x: Reg) -> Reg {
248        self.push(LaneOp::Shr(x, 16))
249    }
250
251    /// A 64-bit operand's four limbs from its two lanes.
252    fn limbs(&mut self, lo: Reg, hi: Reg) -> Vec<Limb> {
253        vec![Limb::Reg(self.lo16(lo)), Limb::Reg(self.hi16(lo)), Limb::Reg(self.lo16(hi)), Limb::Reg(self.hi16(hi))]
254    }
255
256    /// A constant's 16-bit limbs, `count` of them.
257    fn const_limbs(value: u128, count: usize) -> Vec<Limb> {
258        (0..count).map(|i| Limb::K((value >> (16 * i)) as u32 & MASK16)).collect()
259    }
260
261    /// The register holding `limb`; a constant limb becomes a constant lane.
262    fn reg(&mut self, limb: Limb) -> Reg {
263        match limb {
264            Limb::Reg(r) => r,
265            Limb::K(k) => self.konst(k),
266        }
267    }
268
269    /// Sums `terms` into one register; `None` for no terms.
270    fn sum(&mut self, terms: &[Reg]) -> Option<Reg> {
271        let mut acc: Option<Reg> = None;
272        for &t in terms {
273            acc = Some(match acc {
274                None => t,
275                Some(a) => self.push(LaneOp::Add(a, t)),
276            });
277        }
278        acc
279    }
280
281    /// Carries a column sum per limb: limb `k` is `(column_k + carry) mod 2^16` and the carry its quotient. The carry
282    /// out of the last column is computed too, so the algebra can name it, and is not returned.
283    fn carry(&mut self, columns: Vec<Vec<Reg>>) -> Vec<Reg> {
284        let mut out = Vec::with_capacity(columns.len());
285        let mut carry: Option<Reg> = None;
286        for mut terms in columns {
287            terms.extend(carry);
288            let s = match self.sum(&terms) {
289                Some(s) => s,
290                None => self.konst(0),
291            };
292            out.push(self.lo16(s));
293            carry = Some(self.hi16(s));
294        }
295        out
296    }
297
298    /// `a + b` over `n` limbs, modulo 2^(16n).
299    fn add_limbs(&mut self, a: &[Limb], b: &[Limb], n: usize) -> Vec<Reg> {
300        let columns = (0..n)
301            .map(|k| {
302                let mut c = Vec::new();
303                for x in [a.get(k), b.get(k)].into_iter().flatten() {
304                    if !matches!(x, Limb::K(0)) {
305                        c.push(self.reg(*x));
306                    }
307                }
308                c
309            })
310            .collect();
311        self.carry(columns)
312    }
313
314    /// `a − b` over `n` limbs, modulo 2^(16n): `a + (2^16 − 1 − b_k)` limb by limb, plus one.
315    fn sub_limbs(&mut self, a: &[Limb], b: &[Limb], n: usize) -> Vec<Reg> {
316        let mut columns: Vec<Vec<Reg>> = Vec::with_capacity(n);
317        for k in 0..n {
318            let mut c = Vec::new();
319            if let Some(x) = a.get(k) {
320                if !matches!(x, Limb::K(0)) {
321                    c.push(self.reg(*x));
322                }
323            }
324            let complement = match b.get(k) {
325                Some(Limb::K(v)) => self.konst(MASK16 - v),
326                Some(Limb::Reg(r)) => {
327                    let m = self.konst(MASK16);
328                    self.push(LaneOp::Sub(m, *r))
329                }
330                None => self.konst(MASK16),
331            };
332            c.push(complement);
333            if k == 0 {
334                c.push(self.konst(1));
335            }
336            columns.push(c);
337        }
338        self.carry(columns)
339    }
340
341    /// `a · b` over `n` limbs, modulo 2^(16n): every limb product cut in two, its halves added into their columns.
342    fn mul_limbs(&mut self, a: &[Limb], b: &[Limb], n: usize) -> Vec<Reg> {
343        let mut columns: Vec<Vec<Reg>> = vec![Vec::new(); n];
344        for (i, x) in a.iter().enumerate() {
345            for (j, y) in b.iter().enumerate() {
346                if i + j >= n || matches!(x, Limb::K(0)) || matches!(y, Limb::K(0)) {
347                    continue;
348                }
349                let (xr, yr) = (self.reg(*x), self.reg(*y));
350                let p = self.push(LaneOp::MulLo(xr, yr));
351                let (lo, hi) = (self.lo16(p), self.hi16(p));
352                columns[i + j].push(lo);
353                if i + j + 1 < n {
354                    columns[i + j + 1].push(hi);
355                }
356            }
357        }
358        self.carry(columns)
359    }
360
361    /// Two lanes from four limbs.
362    fn join(&mut self, l: &[Reg]) -> [Reg; 2] {
363        let lo_hi = self.push(LaneOp::Shl(l[1], 16));
364        let lo = self.push(LaneOp::Add(l[0], lo_hi));
365        let hi_hi = self.push(LaneOp::Shl(l[3], 16));
366        let hi = self.push(LaneOp::Add(l[2], hi_hi));
367        [lo, hi]
368    }
369}
370
371/// The program that computes `op` on 32-bit lanes.
372pub fn lower(op: Op64) -> LaneProgram {
373    op.check();
374    let mut b = Builder::new();
375    let a_lo = b.push(LaneOp::Input(0));
376    let a_hi = b.push(LaneOp::Input(1));
377    let (b_lo, b_hi) = if op.arity() == 2 { (b.push(LaneOp::Input(2)), b.push(LaneOp::Input(3))) } else { (0, 0) };
378    let mut rem = None;
379    let outputs = match op {
380        Op64::Add | Op64::Sub | Op64::Mul => {
381            let (x, y) = (b.limbs(a_lo, a_hi), b.limbs(b_lo, b_hi));
382            let limbs = match op {
383                Op64::Add => b.add_limbs(&x, &y, 4),
384                Op64::Sub => b.sub_limbs(&x, &y, 4),
385                _ => b.mul_limbs(&x, &y, 4),
386            };
387            b.join(&limbs)
388        }
389        Op64::And => [b.push(LaneOp::And(a_lo, b_lo)), b.push(LaneOp::And(a_hi, b_hi))],
390        Op64::Or => [b.push(LaneOp::Or(a_lo, b_lo)), b.push(LaneOp::Or(a_hi, b_hi))],
391        Op64::Xor => [b.push(LaneOp::Xor(a_lo, b_lo)), b.push(LaneOp::Xor(a_hi, b_hi))],
392        Op64::Shl(k) => match k {
393            0 => [a_lo, a_hi],
394            1..=31 => {
395                let up = b.push(LaneOp::Shl(a_hi, k));
396                let across = b.push(LaneOp::Shr(a_lo, 32 - k));
397                [b.push(LaneOp::Shl(a_lo, k)), b.push(LaneOp::Or(up, across))]
398            }
399            32 => [b.konst(0), a_lo],
400            _ => [b.konst(0), b.push(LaneOp::Shl(a_lo, k - 32))],
401        },
402        Op64::Shr(k) => match k {
403            0 => [a_lo, a_hi],
404            1..=31 => {
405                let down = b.push(LaneOp::Shr(a_lo, k));
406                let across = b.push(LaneOp::Shl(a_hi, 32 - k));
407                [b.push(LaneOp::Or(down, across)), b.push(LaneOp::Shr(a_hi, k))]
408            }
409            32 => [a_hi, b.konst(0)],
410            _ => [b.push(LaneOp::Shr(a_hi, k - 32)), b.konst(0)],
411        },
412        Op64::Rem(1) => [b.konst(0), b.konst(0)],
413        Op64::Rem(d) => {
414            let m = (1u128 << 64) / u128::from(d);
415            let x = b.limbs(a_lo, a_hi);
416            let product = b.mul_limbs(&x, &Builder::const_limbs(m, 4), 8);
417            let q: Vec<Limb> = product[4..].iter().map(|&r| Limb::Reg(r)).collect();
418            let qd = b.mul_limbs(&q, &Builder::const_limbs(u128::from(d), 2), 4);
419            let qd: Vec<Limb> = qd.into_iter().map(Limb::Reg).collect();
420            let diff = b.sub_limbs(&x, &qd, 4);
421            let diff_limbs: Vec<Limb> = diff.iter().map(|&r| Limb::Reg(r)).collect();
422            let probe = b.add_limbs(&diff_limbs, &Builder::const_limbs((1u128 << 34) - u128::from(d), 4), 4);
423            let over = b.push(LaneOp::Shr(probe[2], 2));
424            let dk = b.konst(d);
425            let correction = b.push(LaneOp::MulLo(over, dk));
426            let correction = [Limb::Reg(b.lo16(correction)), Limb::Reg(b.hi16(correction))];
427            let result = b.sub_limbs(&diff_limbs, &correction, 4);
428            rem = Some(RemMarks { product, diff, probe, over });
429            b.join(&result)
430        }
431    };
432    LaneProgram { op, ops: b.ops, outputs, rem }
433}
434
435// =================================================================================================================
436// The algebra
437// =================================================================================================================
438
439/// A symbol: an input lane, or the quotient or the remainder of the `j`-th cut of a value by a power of two.
440#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
441enum Sym {
442    In(usize),
443    Hi(usize),
444    Lo(usize),
445}
446
447/// An integer polynomial in the symbols: each monomial (symbols with their powers, sorted) with its coefficient.
448#[derive(Clone, Debug, PartialEq, Eq, Default)]
449struct Poly(BTreeMap<Vec<(Sym, u32)>, BigInt>);
450
451fn big(x: u128) -> BigInt {
452    BigInt::from_u64((x >> 64) as u64).shl_bits(64).add(&BigInt::from_u64(x as u64))
453}
454
455fn pow2(k: u64) -> BigInt {
456    BigInt::from_i64(1).shl_bits(k)
457}
458
459impl Poly {
460    fn constant(c: BigInt) -> Poly {
461        let mut m = BTreeMap::new();
462        if !c.is_zero() {
463            m.insert(Vec::new(), c);
464        }
465        Poly(m)
466    }
467
468    fn var(s: Sym) -> Poly {
469        let mut m = BTreeMap::new();
470        m.insert(vec![(s, 1)], BigInt::from_i64(1));
471        Poly(m)
472    }
473
474    fn add(&self, o: &Poly) -> Poly {
475        let mut m = self.0.clone();
476        for (mono, c) in &o.0 {
477            let e = m.entry(mono.clone()).or_insert_with(BigInt::zero);
478            *e = e.add(c);
479            if e.is_zero() {
480                m.remove(mono);
481            }
482        }
483        Poly(m)
484    }
485
486    fn scale(&self, k: &BigInt) -> Poly {
487        if k.is_zero() {
488            return Poly::default();
489        }
490        Poly(self.0.iter().map(|(mono, c)| (mono.clone(), c.mul(k))).collect())
491    }
492
493    fn sub(&self, o: &Poly) -> Poly {
494        self.add(&o.scale(&BigInt::from_i64(-1)))
495    }
496
497    fn mul(&self, o: &Poly) -> Poly {
498        let mut out = Poly::default();
499        for (m1, c1) in &self.0 {
500            for (m2, c2) in &o.0 {
501                let mut powers: BTreeMap<Sym, u32> = m1.iter().copied().collect();
502                for &(s, e) in m2 {
503                    *powers.entry(s).or_insert(0) += e;
504                }
505                let mono: Vec<(Sym, u32)> = powers.into_iter().collect();
506                out = out.add(&Poly(BTreeMap::from([(mono, c1.mul(c2))])));
507            }
508        }
509        out
510    }
511
512    /// The polynomial divided by `k`, when every coefficient is a multiple of it.
513    fn div_exact(&self, k: &BigInt) -> Option<Poly> {
514        let mut m = BTreeMap::new();
515        for (mono, c) in &self.0 {
516            let (q, r) = c.div_rem(k)?;
517            if !r.is_zero() {
518                return None;
519            }
520            m.insert(mono.clone(), q);
521        }
522        Some(Poly(m))
523    }
524
525    /// An interval holding every value the polynomial takes with each symbol in its interval.
526    fn interval(&self, iv: &dyn Fn(Sym) -> Interval) -> Interval {
527        let mut out = Interval::point(BigInt::zero());
528        for (mono, c) in &self.0 {
529            let mut term = Interval::point(c.clone());
530            for &(s, e) in mono {
531                for _ in 0..e {
532                    term = term.mul(&iv(s));
533                }
534            }
535            out = out.add(&term);
536        }
537        out
538    }
539
540    /// The polynomial as reflected syntax: a sum of terms, each a coefficient times its symbols.
541    fn syn(&self) -> Syn {
542        let mut terms = self.0.iter().map(|(mono, c)| {
543            let mut t = syn_int(c);
544            for &(s, e) in mono {
545                for _ in 0..e {
546                    t = syn_binary("mul", t, syn_sym(s));
547                }
548            }
549            t
550        });
551        match terms.next() {
552            None => Syn::Lit(0),
553            Some(first) => terms.fold(first, |acc, t| syn_binary("add", acc, t)),
554        }
555    }
556}
557
558fn syn_sym(s: Sym) -> Syn {
559    Syn::Name(match s {
560        Sym::In(i) => format!("in{i}"),
561        Sym::Hi(j) => format!("hi{j}"),
562        Sym::Lo(j) => format!("lo{j}"),
563    })
564}
565
566/// An integer of any size as reflected syntax: literals below 2^32 combined in base 2^32.
567fn syn_int(c: &BigInt) -> Syn {
568    if let Some(v) = c.to_i64() {
569        return Syn::Lit(v);
570    }
571    if c.is_negative() {
572        return syn_binary("sub", Syn::Lit(0), syn_int(&c.negated()));
573    }
574    let base = BigInt::from_u64(1 << 32);
575    let (q, r) = c.div_rem(&base).expect("a nonzero base");
576    syn_binary("add", syn_binary("mul", syn_int(&q), Syn::Lit(1 << 32)), syn_int(&r))
577}
578
579/// A closed interval of integers.
580#[derive(Clone, Debug, PartialEq, Eq)]
581struct Interval {
582    lo: BigInt,
583    hi: BigInt,
584}
585
586impl Interval {
587    fn new(lo: BigInt, hi: BigInt) -> Interval {
588        Interval { lo, hi }
589    }
590
591    fn point(v: BigInt) -> Interval {
592        Interval { lo: v.clone(), hi: v }
593    }
594
595    fn add(&self, o: &Interval) -> Interval {
596        Interval::new(self.lo.add(&o.lo), self.hi.add(&o.hi))
597    }
598
599    fn mul(&self, o: &Interval) -> Interval {
600        let c = [self.lo.mul(&o.lo), self.lo.mul(&o.hi), self.hi.mul(&o.lo), self.hi.mul(&o.hi)];
601        Interval::new(c.iter().min().unwrap().clone(), c.iter().max().unwrap().clone())
602    }
603
604    fn within(&self, lo: &BigInt, hi: &BigInt) -> bool {
605        &self.lo >= lo && &self.hi <= hi
606    }
607}
608
609/// A cut `value = hi · 2^k + lo`, `0 ≤ lo < 2^k`.
610#[derive(Clone, Debug)]
611struct Cut {
612    of: Reg,
613    k: u32,
614    hi: Interval,
615    lo: Interval,
616}
617
618/// A register's value: as a polynomial in the inputs and the cuts' quotients and remainders (`atom`, the form its
619/// interval is read from), the same with every remainder written as `value − hi · 2^k` (`elim`, in the inputs and the
620/// quotients only), and that form as the expression tree the kernel normalizes.
621#[derive(Clone, Debug)]
622struct Value {
623    atom: Poly,
624    elim: Poly,
625    tree: Syn,
626    iv: Interval,
627}
628
629/// The program evaluated symbolically, every operation shown not to wrap.
630struct Algebra {
631    values: Vec<Value>,
632    cuts: Vec<Cut>,
633}
634
635fn lane_max() -> BigInt {
636    BigInt::from_u64(u64::from(u32::MAX))
637}
638
639impl Algebra {
640    fn symbol_interval(&self, s: Sym) -> Interval {
641        match s {
642            Sym::In(_) => Interval::new(BigInt::zero(), lane_max()),
643            Sym::Hi(j) => self.cuts[j].hi.clone(),
644            Sym::Lo(j) => self.cuts[j].lo.clone(),
645        }
646    }
647
648    /// The interval of a polynomial in atom form.
649    fn bound(&self, p: &Poly) -> Interval {
650        p.interval(&|s| self.symbol_interval(s))
651    }
652
653    /// Evaluates `program`. A shift right named in `narrowed` has its quotient in the interval given there, which the
654    /// caller has established from what precedes it; every other interval is the operation's own.
655    fn run(program: &LaneProgram, narrowed: &BTreeMap<Reg, Interval>) -> Result<Algebra, String> {
656        let mut alg = Algebra { values: Vec::new(), cuts: Vec::new() };
657        let mut memo: BTreeMap<(Reg, u32), usize> = BTreeMap::new();
658        let ops = &program.ops;
659        for (r, op) in ops.iter().enumerate() {
660            let exact = |iv: &Interval, what: &str| -> Result<(), String> {
661                if iv.within(&BigInt::zero(), &lane_max()) {
662                    Ok(())
663                } else {
664                    Err(format!("register {r} ({what}) can leave a 32-bit lane: [{}, {}]", iv.lo, iv.hi))
665                }
666            };
667            let value = match *op {
668                LaneOp::Input(i) => Value {
669                    atom: Poly::var(Sym::In(i)),
670                    elim: Poly::var(Sym::In(i)),
671                    tree: syn_sym(Sym::In(i)),
672                    iv: Interval::new(BigInt::zero(), lane_max()),
673                },
674                LaneOp::Const(c) => {
675                    let c = BigInt::from_u64(u64::from(c));
676                    Value { atom: Poly::constant(c.clone()), elim: Poly::constant(c.clone()), tree: syn_int(&c), iv: Interval::point(c) }
677                }
678                LaneOp::Add(a, b) => {
679                    let (x, y) = (&alg.values[a], &alg.values[b]);
680                    let iv = x.iv.add(&y.iv);
681                    exact(&iv, "an add")?;
682                    Value {
683                        atom: x.atom.add(&y.atom),
684                        elim: x.elim.add(&y.elim),
685                        tree: syn_binary("add", x.tree.clone(), y.tree.clone()),
686                        iv,
687                    }
688                }
689                LaneOp::Sub(a, b) => {
690                    let (x, y) = (&alg.values[a], &alg.values[b]);
691                    let iv = Interval::new(x.iv.lo.sub(&y.iv.hi), x.iv.hi.sub(&y.iv.lo));
692                    exact(&iv, "a subtract")?;
693                    Value {
694                        atom: x.atom.sub(&y.atom),
695                        elim: x.elim.sub(&y.elim),
696                        tree: syn_binary("sub", x.tree.clone(), y.tree.clone()),
697                        iv,
698                    }
699                }
700                LaneOp::MulLo(a, b) => {
701                    let (x, y) = (&alg.values[a], &alg.values[b]);
702                    let iv = x.iv.mul(&y.iv);
703                    exact(&iv, "a multiply")?;
704                    Value {
705                        atom: x.atom.mul(&y.atom),
706                        elim: x.elim.mul(&y.elim),
707                        tree: syn_binary("mul", x.tree.clone(), y.tree.clone()),
708                        iv,
709                    }
710                }
711                LaneOp::Shl(a, k) => {
712                    let x = &alg.values[a];
713                    let f = pow2(u64::from(k));
714                    let iv = x.iv.mul(&Interval::point(f.clone()));
715                    exact(&iv, "a shift left")?;
716                    Value { atom: x.atom.scale(&f), elim: x.elim.scale(&f), tree: syn_binary("mul", x.tree.clone(), syn_int(&f)), iv }
717                }
718                LaneOp::Shr(a, k) => {
719                    let (hi, _) = alg.cut(&mut memo, a, k);
720                    match narrowed.get(&r) {
721                        None => hi,
722                        Some(iv) => {
723                            let j = memo[&(a, k)];
724                            if !iv.within(&alg.cuts[j].hi.lo, &alg.cuts[j].hi.hi) {
725                                return Err(format!("register {r}: a narrowing outside the quotient's own interval"));
726                            }
727                            alg.cuts[j].hi = iv.clone();
728                            Value { iv: iv.clone(), ..hi }
729                        }
730                    }
731                }
732                LaneOp::And(a, m) => {
733                    let mask = match ops[m] {
734                        LaneOp::Const(c) if c != 0 && (c & c.wrapping_add(1)) == 0 => c,
735                        _ => return Err(format!("register {r}: an and with no low mask is not arithmetic")),
736                    };
737                    alg.cut(&mut memo, a, 32 - mask.leading_zeros()).1
738                }
739                LaneOp::Or(..) | LaneOp::Xor(..) => return Err(format!("register {r}: a bitwise operation is not arithmetic")),
740            };
741            alg.values.push(value);
742        }
743        Ok(alg)
744    }
745
746    /// The cut of register `a` by `2^k`: its quotient's value and its remainder's.
747    fn cut(&mut self, memo: &mut BTreeMap<(Reg, u32), usize>, a: Reg, k: u32) -> (Value, Value) {
748        let j = match memo.get(&(a, k)) {
749            Some(&j) => j,
750            None => {
751                let iv = &self.values[a].iv;
752                let f = pow2(u64::from(k));
753                let (hi_top, _) = iv.hi.div_rem(&f).expect("a power of two");
754                let (hi_bottom, _) = iv.lo.div_rem(&f).expect("a power of two");
755                let lo_top = if iv.hi < f { iv.hi.clone() } else { f.sub(&BigInt::from_i64(1)) };
756                self.cuts.push(Cut {
757                    of: a,
758                    k,
759                    hi: Interval::new(hi_bottom, hi_top),
760                    lo: Interval::new(BigInt::zero(), lo_top),
761                });
762                memo.insert((a, k), self.cuts.len() - 1);
763                self.cuts.len() - 1
764            }
765        };
766        let f = pow2(u64::from(k));
767        let hi = Value {
768            atom: Poly::var(Sym::Hi(j)),
769            elim: Poly::var(Sym::Hi(j)),
770            tree: syn_sym(Sym::Hi(j)),
771            iv: self.symbol_interval(Sym::Hi(j)),
772        };
773        let of = &self.values[a];
774        let lo = Value {
775            atom: Poly::var(Sym::Lo(j)),
776            elim: of.elim.sub(&Poly::var(Sym::Hi(j)).scale(&f)),
777            tree: syn_binary("sub", of.tree.clone(), syn_binary("mul", syn_sym(Sym::Hi(j)), syn_int(&f))),
778            iv: self.cuts[j].lo.clone(),
779        };
780        (hi, lo)
781    }
782
783    /// `Σ limb_k · 2^(16k)` over registers, in each form.
784    fn limb_sum(&self, limbs: &[Reg]) -> (Poly, Poly, Syn) {
785        let mut atom = Poly::default();
786        let mut elim = Poly::default();
787        let mut tree: Option<Syn> = None;
788        for (k, &r) in limbs.iter().enumerate() {
789            let f = pow2(16 * k as u64);
790            atom = atom.add(&self.values[r].atom.scale(&f));
791            elim = elim.add(&self.values[r].elim.scale(&f));
792            let t = syn_binary("mul", self.values[r].tree.clone(), syn_int(&f));
793            tree = Some(match tree {
794                None => t,
795                Some(acc) => syn_binary("add", acc, t),
796            });
797        }
798        (atom, elim, tree.unwrap_or(Syn::Lit(0)))
799    }
800
801    /// The two output lanes as one 64-bit value, in each form.
802    fn result(&self, program: &LaneProgram) -> (Poly, Poly, Syn) {
803        let [lo, hi] = program.outputs;
804        let f = pow2(32);
805        let atom = self.values[lo].atom.add(&self.values[hi].atom.scale(&f));
806        let elim = self.values[lo].elim.add(&self.values[hi].elim.scale(&f));
807        let tree = syn_binary("add", self.values[lo].tree.clone(), syn_binary("mul", self.values[hi].tree.clone(), syn_int(&f)));
808        (atom, elim, tree)
809    }
810}
811
812/// An operand as a polynomial in the input lanes: `in(2i) + in(2i+1) · 2^32`.
813fn operand(i: usize) -> Poly {
814    Poly::var(Sym::In(2 * i)).add(&Poly::var(Sym::In(2 * i + 1)).scale(&pow2(32)))
815}
816
817/// One identity the kernels check: `lhs = rhs` over the integers.
818pub type Identity = (Syn, Syn);
819
820/// `tree = spec + 2^64 · K` for the integer polynomial `K` that makes it hold, when there is one.
821fn modular_identity(elim: &Poly, tree: Syn, spec: &Poly) -> Result<(Identity, Poly), String> {
822    let modulus = pow2(64);
823    let k = elim.sub(spec).div_exact(&modulus).ok_or("the result differs from its definition modulo 2^64")?;
824    let rhs = spec.add(&k.scale(&modulus));
825    Ok(((tree, rhs.syn()), k))
826}
827
828/// The identities that prove an add, subtract or multiply.
829fn ring_identities(program: &LaneProgram) -> Result<Vec<Identity>, String> {
830    let alg = Algebra::run(program, &BTreeMap::new())?;
831    let (_, elim, tree) = alg.result(program);
832    let (a, b) = (operand(0), operand(1));
833    let spec = match program.op {
834        Op64::Add => a.add(&b),
835        Op64::Sub => a.sub(&b),
836        Op64::Mul => a.mul(&b),
837        op => return Err(format!("{op:?} is not a ring operation")),
838    };
839    Ok(vec![modular_identity(&elim, tree, &spec)?.0])
840}
841
842/// Whether two values congruent modulo 2^64 are equal: both lie in `[0, 2^64)`.
843fn congruent_means_equal(a: &Interval, b: &Interval) -> bool {
844    let top = pow2(64).sub(&BigInt::from_i64(1));
845    a.within(&BigInt::zero(), &top) && b.within(&BigInt::zero(), &top)
846}
847
848/// The facts that prove a remainder by `d ≥ 2`: the identities the kernels check, and the intervals checked here that
849/// turn congruences into equalities and fix the correction bit.
850///
851/// With `m = ⌊2^64/d⌋` and `e = 2^64 − d·m < d`:
852/// 1. the product chain's limbs are `x·m − c₁·2^128`, `c₁` its carry out, and `c₁ = 0` because `x·m < 2^128` and
853///    the limbs are not negative; so `x·m = q·2^64 + R` with `q` the high four limbs and `R ∈ [0, 2^64)` the low;
854/// 2. Barrett's difference `T = x − q·d` satisfies `T·2^64 = x·e + d·R` (plus `d·c₁·2^128`, zero), so
855///    `T ∈ [0, 2d)`;
856/// 3. the program's difference is `T` modulo 2^64, and both are in `[0, 2^64)`: it is `T`;
857/// 4. the probe is `T + 2^34 − d` modulo 2^64, both in `[0, 2^64)`: it is that, in `[2^34 − d, 2^34 + d)`, below
858///    2^35; its limb 2 over 4 is the correction bit, then 0 or 1, and its top limb is zero;
859/// 5. the result is the difference less `bit · d` modulo 2^64; with the bit 0 the probe is below 2^34 and `T < d`,
860///    with the bit 1 it is at least 2^34 and `T ∈ [d, 2d)`; either way `T − bit·d ∈ [0, d)` and the result, in
861///    `[0, 2^64)`, is it. So the result is `x − (q + bit)·d ∈ [0, d)`: `x mod d`.
862fn remainder_identities(program: &LaneProgram, d: u32) -> Result<Vec<Identity>, String> {
863    let marks = program.rem.as_ref().ok_or("a remainder's program without its marks")?;
864    let one = BigInt::from_i64(1);
865    let d_big = BigInt::from_u64(u64::from(d));
866    let two_d = d_big.add(&d_big);
867    let m = (1u128 << 64) / u128::from(d);
868    let (m_big, e_big) = (big(m), big((1u128 << 64) - m * u128::from(d)));
869    let (two64, two128) = (pow2(64), pow2(128));
870    let x = operand(0);
871    let mut identities = Vec::new();
872
873    // The correction bit's interval, [0, 1], is established in step 4 from what precedes its use.
874    let bit = Interval::new(BigInt::zero(), one.clone());
875    let alg = Algebra::run(program, &BTreeMap::from([(marks.over, bit)]))?;
876
877    // (1)
878    let (p_atom, p_elim, p_tree) = alg.limb_sum(&marks.product);
879    let top = *marks.product.last().ok_or("an empty product")?;
880    let top_cut = match alg.values[top].atom.0.keys().next().map(|m| m.as_slice()) {
881        Some([(Sym::Lo(j), 1)]) if alg.values[top].atom.0.len() == 1 => *j,
882        _ => return Err("the product's top limb is not a cut's remainder".into()),
883    };
884    let carry_out = Poly::var(Sym::Hi(top_cut));
885    let xm = x.scale(&m_big);
886    let claim = xm.sub(&carry_out.scale(&two128));
887    if p_elim != claim {
888        return Err("the product chain is not x · m".into());
889    }
890    identities.push((p_tree, claim.syn()));
891    if alg.bound(&xm).hi >= two128 || alg.bound(&p_atom).lo.is_negative() || alg.cuts[top_cut].hi.lo.is_negative() {
892        return Err("the product's carry out is not shown zero".into());
893    }
894
895    // (2)
896    let (q_atom, q_elim, q_tree) = alg.limb_sum(&marks.product[4..]);
897    let (r_atom, r_elim, r_tree) = alg.limb_sum(&marks.product[..4]);
898    let t_elim = x.sub(&q_elim.scale(&d_big));
899    let barrett_lhs = t_elim.scale(&two64);
900    let barrett_rhs = x.scale(&e_big).add(&r_elim.scale(&d_big)).add(&carry_out.scale(&d_big.mul(&two128)));
901    if barrett_lhs != barrett_rhs {
902        return Err("Barrett's identity does not hold".into());
903    }
904    identities.push((
905        syn_binary("mul", syn_binary("sub", x.syn(), syn_binary("mul", q_tree, syn_int(&d_big))), syn_int(&two64)),
906        syn_binary(
907            "add",
908            syn_binary("add", syn_binary("mul", x.syn(), syn_int(&e_big)), syn_binary("mul", syn_int(&d_big), r_tree)),
909            syn_binary("mul", carry_out.syn(), syn_int(&d_big.mul(&two128))),
910        ),
911    ));
912    let (x_bound, r_bound) = (alg.bound(&x), alg.bound(&r_atom));
913    if x_bound.lo.is_negative() || r_bound.lo.is_negative() || r_bound.hi >= two64 || alg.bound(&q_atom).lo.is_negative() {
914        return Err("Barrett's terms are not bounded".into());
915    }
916    // T · 2^64 = x·e + d·R, each term in [0, …]: T ∈ [0, ⌊(x_max·e + d·R_max) / 2^64⌋], which is below 2d as e < d.
917    let (t_top, _) = x_bound.hi.mul(&e_big).add(&d_big.mul(&r_bound.hi)).div_rem(&two64).ok_or("a zero modulus")?;
918    if t_top >= two_d {
919        return Err("Barrett's difference is not shown below 2d".into());
920    }
921    let t = Interval::new(BigInt::zero(), t_top);
922
923    // (3)
924    let (diff_atom, diff_elim, diff_tree) = alg.limb_sum(&marks.diff);
925    identities.push(modular_identity(&diff_elim, diff_tree, &t_elim)?.0);
926    if !congruent_means_equal(&alg.bound(&diff_atom), &t) {
927        return Err("the difference is not shown to be Barrett's".into());
928    }
929
930    // (4)
931    let (probe_atom, probe_elim, probe_tree) = alg.limb_sum(&marks.probe);
932    let shift = big((1u128 << 34) - u128::from(d));
933    identities.push(modular_identity(&probe_elim, probe_tree, &diff_elim.add(&Poly::constant(shift.clone())))?.0);
934    let probe = Interval::new(t.lo.add(&shift), t.hi.add(&shift));
935    if !congruent_means_equal(&alg.bound(&probe_atom), &probe) || probe.hi >= pow2(35) {
936        return Err("the probe is not shown exact and below 2^35".into());
937    }
938    let over = match alg.values[marks.over].atom.0.keys().next().map(|m| m.as_slice()) {
939        Some([(Sym::Hi(j), 1)]) => *j,
940        _ => return Err("the correction bit is not a cut's quotient".into()),
941    };
942    let limb2 = marks.probe[2];
943    if alg.cuts[over].of != limb2 || alg.cuts[over].k != 2 {
944        return Err("the correction bit is not the probe's limb 2 over 4".into());
945    }
946    // probe ≥ limb2 · 2^32 ≥ bit · 2^34, the other limbs not negative; with probe < 2^35 the bit is at most 1. And the
947    // top limb is zero: top · 2^48 ≤ probe < 2^48.
948    let (bit_top, _) = probe.hi.div_rem(&pow2(34)).ok_or("a zero modulus")?;
949    if bit_top > one || probe.hi >= pow2(48) {
950        return Err("the correction bit is not shown to be one bit".into());
951    }
952
953    // (5)
954    let (out_atom, out_elim, out_tree) = alg.result(program);
955    let over_sym = Poly::var(Sym::Hi(over));
956    identities.push(modular_identity(&out_elim, out_tree, &diff_elim.sub(&over_sym.scale(&d_big)))?.0);
957    let out_bound = alg.bound(&out_atom);
958    for b in [0i64, 1] {
959        // The probe with the bit b: limb 2 ∈ [4b, 4b + 3], limbs 0 and 1 below 2^16, the top limb zero.
960        let bit_scale = BigInt::from_i64(b).mul(&pow2(34));
961        let case = Interval::new(
962            std::cmp::max(probe.lo.clone(), bit_scale.clone()),
963            std::cmp::min(probe.hi.clone(), bit_scale.add(&pow2(34)).sub(&one)),
964        );
965        if case.lo > case.hi {
966            continue;
967        }
968        // T = probe − 2^34 + d, less b·d.
969        let less = d_big.mul(&BigInt::from_i64(b));
970        let value = Interval::new(case.lo.sub(&shift).sub(&less), case.hi.sub(&shift).sub(&less));
971        if value.lo.is_negative() || value.hi >= d_big || !congruent_means_equal(&out_bound, &value) {
972            return Err(format!("with the correction bit {b}, the result is not shown in [0, d)"));
973        }
974    }
975    Ok(identities)
976}
977
978/// What the kernels say of one identity.
979#[derive(Clone, Copy, Debug, PartialEq, Eq)]
980pub enum KernelVerdict {
981    /// The first kernel's `try_ring` reduces it to `DRingSolve` and its conclusion is the identity, and the second
982    /// kernel re-checks the same.
983    Both,
984    /// The first kernel accepts it; the second's exact evaluation (a Kronecker substitution with a bound on its
985    /// slots) declines it as past its bounds, which is no verdict and never a disagreement.
986    FirstOnly,
987    /// The first kernel rejects it, or the two disagree.
988    Rejected,
989}
990
991/// The kernels' verdict on `lhs = rhs` over the integers: the witness that `concludes (try_ring goal)` is `goal` (the
992/// first kernel reduces `try_ring` to `DRingSolve` by ring normalization, then `concludes` re-verifies it) checked by
993/// `infer_type`, and re-checked by the second kernel through `double_check`.
994fn kernels_on(ctx: &Context, (lhs, rhs): &Identity) -> KernelVerdict {
995    let goal = term_of_syn(&syn_typed_eq("Int", lhs.clone(), rhs.clone()));
996    let g = |n: &str| Term::Global(n.into());
997    let app = |f: Term, a: Term| Term::App(Box::new(f), Box::new(a));
998    let conclusion = app(g("concludes"), app(g(CertifiablePrimitive::Ring.rule_name()), goal.clone()));
999    let ty = app(app(app(g("Eq"), g("Syntax")), conclusion), goal.clone());
1000    let witness = app(
1001        Term::Lambda { param: "proof".into(), param_type: Box::new(ty), body: Box::new(Term::Var("proof".into())) },
1002        app(app(g("refl"), g("Syntax")), goal),
1003    );
1004    if infer_type(ctx, &witness).is_err() {
1005        return KernelVerdict::Rejected;
1006    }
1007    match double_check(ctx, &witness) {
1008        DoubleCheck::Agreed => KernelVerdict::Both,
1009        DoubleCheck::MainOnlyReCheckerIncomplete(_) => KernelVerdict::FirstOnly,
1010        DoubleCheck::Disagree(_) => KernelVerdict::Rejected,
1011    }
1012}
1013
1014// =================================================================================================================
1015// The circuits
1016// =================================================================================================================
1017
1018/// The program's outputs as a circuit over the operands' bits (`a0` … `a63`, `b0` … `b63`), outputs `o0` … `o63`.
1019fn program_circuit(program: &LaneProgram) -> Result<Aig, String> {
1020    let mut g = Aig::new();
1021    let mut operand_bits: Vec<Vec<AigLit>> = Vec::new();
1022    for o in 0..program.op.arity() {
1023        let name = if o == 0 { "a" } else { "b" };
1024        operand_bits.push((0..64).map(|i| g.add_input(Some(format!("{name}{i}")))).collect());
1025    }
1026    let mut lanes: Vec<Vec<AigLit>> = Vec::new();
1027    for (r, op) in program.ops.iter().enumerate() {
1028        let bits: Vec<AigLit> = match *op {
1029            LaneOp::Input(i) => operand_bits[i / 2][32 * (i % 2)..32 * (i % 2) + 32].to_vec(),
1030            LaneOp::Const(c) => (0..32).map(|i| if c >> i & 1 == 1 { AigLit::TRUE } else { AigLit::FALSE }).collect(),
1031            LaneOp::And(a, b) => (0..32).map(|i| g.and(lanes[a][i], lanes[b][i])).collect(),
1032            LaneOp::Or(a, b) => (0..32).map(|i| g.or(lanes[a][i], lanes[b][i])).collect(),
1033            LaneOp::Xor(a, b) => (0..32).map(|i| g.xor(lanes[a][i], lanes[b][i])).collect(),
1034            LaneOp::Shl(a, k) => (0..32).map(|i| if i >= k as usize { lanes[a][i - k as usize] } else { AigLit::FALSE }).collect(),
1035            LaneOp::Shr(a, k) => (0..32).map(|i| if i + (k as usize) < 32 { lanes[a][i + k as usize] } else { AigLit::FALSE }).collect(),
1036            LaneOp::Add(..) | LaneOp::Sub(..) | LaneOp::MulLo(..) => {
1037                return Err(format!("register {r}: arithmetic in a wiring program"));
1038            }
1039        };
1040        lanes.push(bits);
1041    }
1042    for (half, &r) in program.outputs.iter().enumerate() {
1043        for i in 0..32 {
1044            g.add_output(lanes[r][i], Some(format!("o{}", 32 * half + i)));
1045        }
1046    }
1047    Ok(g)
1048}
1049
1050/// The operation's definition as a circuit over the same named bits.
1051fn definition_circuit(op: Op64) -> Aig {
1052    let mut g = Aig::new();
1053    let a: Vec<AigLit> = (0..64).map(|i| g.add_input(Some(format!("a{i}")))).collect();
1054    let b: Vec<AigLit> =
1055        if op.arity() == 2 { (0..64).map(|i| g.add_input(Some(format!("b{i}")))).collect() } else { Vec::new() };
1056    let out: Vec<AigLit> = (0..64)
1057        .map(|i| match op {
1058            Op64::And => g.and(a[i], b[i]),
1059            Op64::Or => g.or(a[i], b[i]),
1060            Op64::Xor => g.xor(a[i], b[i]),
1061            Op64::Shl(k) => {
1062                if i >= k as usize {
1063                    a[i - k as usize]
1064                } else {
1065                    AigLit::FALSE
1066                }
1067            }
1068            Op64::Shr(k) => {
1069                if i + (k as usize) < 64 {
1070                    a[i + k as usize]
1071                } else {
1072                    AigLit::FALSE
1073                }
1074            }
1075            _ => unreachable!("only wiring has a circuit definition"),
1076        })
1077        .collect();
1078    for (i, l) in out.into_iter().enumerate() {
1079        g.add_output(l, Some(format!("o{i}")));
1080    }
1081    g
1082}
1083
1084/// Whether the program's circuit and the definition's are equal, with a derivation that replays.
1085fn circuits_agree(program: &LaneProgram) -> Result<bool, String> {
1086    let left = program_circuit(program)?;
1087    let right = definition_circuit(program.op);
1088    let report = fraig::equivalent(&left, &right, &fraig::Options::default()).map_err(|e| format!("{e:?}"))?;
1089    Ok(report.verdicts.len() == 64 && report.all_equal() && report.check())
1090}
1091
1092// =================================================================================================================
1093// Proofs
1094// =================================================================================================================
1095
1096/// How a lowering is proved.
1097#[derive(Clone, Debug, PartialEq, Eq)]
1098pub enum ProofKind {
1099    /// Identities over the integers, each with what the kernels said of it, and the intervals (checked here) that make
1100    /// every lane operation exact and turn the identities' congruences into equalities.
1101    Algebra(Vec<(Identity, KernelVerdict)>),
1102    /// A certified equivalence check of the bit-blasted program against the bit-blasted definition.
1103    Circuit,
1104}
1105
1106/// A lowering's proof: the program, and how it is proved equal to its 64-bit definition.
1107#[derive(Clone, Debug)]
1108pub struct Proof {
1109    program: LaneProgram,
1110    kind: ProofKind,
1111}
1112
1113impl Proof {
1114    pub fn kind(&self) -> &ProofKind {
1115        &self.kind
1116    }
1117
1118    pub fn program(&self) -> &LaneProgram {
1119        &self.program
1120    }
1121
1122    /// Checks the proof afresh: the program evaluated symbolically again, its identities and intervals derived again,
1123    /// the identities equal to those the proof carries, each accepted by the first kernel and never contradicted by the
1124    /// second, with the verdicts the proof records; or the circuits' equivalence decided again and its derivation
1125    /// replayed.
1126    pub fn check(&self) -> bool {
1127        if self.program != lower(self.program.op) {
1128            return false;
1129        }
1130        match &self.kind {
1131            ProofKind::Circuit => circuits_agree(&self.program).unwrap_or(false),
1132            ProofKind::Algebra(recorded) => {
1133                let derived = match identities_of(&self.program) {
1134                    Ok(Some(ids)) => ids,
1135                    _ => return false,
1136                };
1137                if derived.len() != recorded.len() || derived.iter().zip(recorded).any(|(d, (r, _))| d != r) {
1138                    return false;
1139                }
1140                KERNEL.with(|ctx| {
1141                    recorded.iter().all(|(id, verdict)| {
1142                        let now = kernels_on(ctx, id);
1143                        now != KernelVerdict::Rejected && now == *verdict
1144                    })
1145                })
1146            }
1147        }
1148    }
1149
1150    /// The same program with `kind` in place of its proof, to be checked.
1151    pub fn with_kind(&self, kind: ProofKind) -> Proof {
1152        Proof { program: self.program.clone(), kind }
1153    }
1154
1155    /// The identities both kernels decided, and all of them; zero and zero for a circuit proof.
1156    pub fn second_kernel_decided(&self) -> (usize, usize) {
1157        match &self.kind {
1158            ProofKind::Algebra(ids) => (ids.iter().filter(|(_, v)| *v == KernelVerdict::Both).count(), ids.len()),
1159            ProofKind::Circuit => (0, 0),
1160        }
1161    }
1162}
1163
1164/// The identities that prove an arithmetic lowering; `None` for a wiring one.
1165fn identities_of(program: &LaneProgram) -> Result<Option<Vec<Identity>>, String> {
1166    match program.op {
1167        Op64::Add | Op64::Sub | Op64::Mul => ring_identities(program).map(Some),
1168        Op64::Rem(1) => {
1169            let alg = Algebra::run(program, &BTreeMap::new())?;
1170            let (_, elim, tree) = alg.result(program);
1171            if !elim.0.is_empty() {
1172                return Err("a remainder by one is not zero".into());
1173            }
1174            Ok(Some(vec![(tree, Syn::Lit(0))]))
1175        }
1176        Op64::Rem(d) => remainder_identities(program, d).map(Some),
1177        Op64::And | Op64::Or | Op64::Xor | Op64::Shl(_) | Op64::Shr(_) => Ok(None),
1178    }
1179}
1180
1181thread_local! {
1182    /// The kernel's standard library, registered once a thread: building it is most of what checking one identity costs.
1183    static KERNEL: Context = {
1184        let mut ctx = Context::new();
1185        StandardLibrary::register(&mut ctx);
1186        ctx
1187    };
1188}
1189
1190/// The lowering of `op` and its proof.
1191pub fn prove(op: Op64) -> Result<Proof, String> {
1192    prove_program(lower(op))
1193}
1194
1195/// A proof that `program` computes its operation, or why there is none. [`Proof::check`] accepts only a proof of the
1196/// lowering itself; this is the prover any program can be put to.
1197pub fn prove_program(program: LaneProgram) -> Result<Proof, String> {
1198    let op = program.op;
1199    let kind = match identities_of(&program)? {
1200        Some(ids) => {
1201            let mut verdicts = Vec::with_capacity(ids.len());
1202            for (i, id) in ids.into_iter().enumerate() {
1203                let verdict = KERNEL.with(|ctx| kernels_on(ctx, &id));
1204                if verdict == KernelVerdict::Rejected {
1205                    return Err(format!("{op:?}: the kernel rejects identity {i}"));
1206                }
1207                verdicts.push((id, verdict));
1208            }
1209            ProofKind::Algebra(verdicts)
1210        }
1211        None => {
1212            if !circuits_agree(&program)? {
1213                return Err(format!("{op:?}: the program's circuit and the definition's differ"));
1214            }
1215            ProofKind::Circuit
1216        }
1217    };
1218    let proof = Proof { program, kind };
1219    Ok(proof)
1220}