Skip to main content

hekate_program/
predicate.rs

1// SPDX-FileCopyrightText: 2026 Andrei Kochergin <andrei@oumuamua.dev>
2// SPDX-FileCopyrightText: 2026 Oumuamua Labs <info@oumuamua.dev>
3// SPDX-License-Identifier: AGPL-3.0-only
4
5use crate::ProgramCell;
6use crate::constraint::{ConstraintAst, ConstraintExpr, ExprId};
7use alloc::vec;
8use alloc::vec::Vec;
9use hekate_math::{Flat, HardwareField, TowerField};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum WireRole {
13    Lhs = 0,
14    Rhs = 1,
15    Product = 2,
16}
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum Unknown {
20    Pad(u32),
21    Wire { mul: u32, role: WireRole },
22}
23
24impl Unknown {
25    pub fn shifted(self, mul_offset: u32) -> Self {
26        match self {
27            Unknown::Pad(_) => self,
28            Unknown::Wire { mul, role } => Unknown::Wire {
29                mul: mul + mul_offset,
30                role,
31            },
32        }
33    }
34}
35
36/// `half` is `V + K`.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct ClaimLayout {
39    pub pad_first: u32,
40    pub half: u32,
41}
42
43impl ClaimLayout {
44    pub fn index(&self, cell: ProgramCell) -> u32 {
45        let base = cell.col_idx as u32;
46
47        match cell.next_row {
48            true => base + self.half,
49            false => base,
50        }
51    }
52}
53
54/// `Σ unknowns = Σ claims + constant`.
55#[derive(Clone, Debug)]
56pub struct AffineRow<F> {
57    pub unknowns: Vec<(Unknown, Flat<F>)>,
58    pub claims: Vec<(u32, Flat<F>)>,
59    pub constant: Flat<F>,
60}
61
62/// Hadamard rows are implicit:
63/// `Lhs · Rhs = Product` for each `mul` in `0..mul_nodes`.
64#[derive(Clone, Debug)]
65pub struct PredicateRows<F> {
66    pub affine: Vec<AffineRow<F>>,
67    pub mul_nodes: u32,
68    pub roots: Vec<Form<F>>,
69}
70
71#[derive(Clone, Debug)]
72pub struct Form<F> {
73    pub unknowns: Vec<(Unknown, Flat<F>)>,
74    pub claims: Vec<(u32, Flat<F>)>,
75    pub constant: Flat<F>,
76}
77
78impl<F: TowerField> Default for Form<F> {
79    fn default() -> Self {
80        Self {
81            unknowns: Vec::new(),
82            claims: Vec::new(),
83            constant: Flat::from_raw(F::ZERO),
84        }
85    }
86}
87
88impl<F: HardwareField> Form<F> {
89    fn extend(&mut self, other: &Self) {
90        self.unknowns.extend_from_slice(&other.unknowns);
91        self.claims.extend_from_slice(&other.claims);
92
93        self.constant += other.constant;
94    }
95
96    fn scaled(&self, coeff: Flat<F>) -> Self {
97        Self {
98            unknowns: self.unknowns.iter().map(|&(v, c)| (v, c * coeff)).collect(),
99            claims: self.claims.iter().map(|&(v, c)| (v, c * coeff)).collect(),
100            constant: self.constant * coeff,
101        }
102    }
103
104    pub fn evaluate(&self, claims: &[Flat<F>], pad: &[Flat<F>], wires: &[[Flat<F>; 3]]) -> Flat<F> {
105        let mut acc = self.constant;
106        for &(idx, coeff) in &self.claims {
107            acc += coeff * claims[idx as usize];
108        }
109
110        for &(unknown, coeff) in &self.unknowns {
111            let v = match unknown {
112                Unknown::Pad(i) => pad[i as usize],
113                Unknown::Wire { mul, role } => wires[mul as usize][role as usize],
114            };
115
116            acc += coeff * v;
117        }
118
119        acc
120    }
121}
122
123/// Honest `(lhs, rhs, product)` per reachable `Mul`,
124/// numbered exactly as [`compile`] numbers its wires.
125pub fn wire_values<F: TowerField + HardwareField>(
126    ast: &ConstraintAst<F>,
127    node_values: &[Flat<F>],
128) -> Vec<[Flat<F>; 3]> {
129    let live = crate::outer::reachable(ast);
130    let mut wires = Vec::new();
131
132    for (i, &alive) in live.iter().enumerate() {
133        if !alive {
134            continue;
135        }
136
137        if let ConstraintExpr::Mul(a, b) = ast.arena.get(ExprId(i as u32)) {
138            wires.push([
139                node_values[a.0 as usize],
140                node_values[b.0 as usize],
141                node_values[i],
142            ]);
143        }
144    }
145
146    wires
147}
148
149pub fn compile<F: HardwareField>(ast: &ConstraintAst<F>, layout: ClaimLayout) -> PredicateRows<F> {
150    let n = ast.arena.len();
151    let live = crate::outer::reachable(ast);
152    let one = Flat::from_raw(F::ONE);
153
154    let mut forms: Vec<Form<F>> = Vec::with_capacity(n);
155    let mut affine: Vec<AffineRow<F>> = Vec::new();
156    let mut mul_nodes: u32 = 0;
157
158    for (i, &alive) in live.iter().enumerate() {
159        if !alive {
160            forms.push(Form::default());
161            continue;
162        }
163
164        let form = match ast.arena.get(ExprId(i as u32)) {
165            ConstraintExpr::Cell(cell) => {
166                let idx = layout.index(*cell);
167
168                Form {
169                    unknowns: vec![(Unknown::Pad(layout.pad_first + idx), one)],
170                    claims: vec![(idx, one)],
171                    constant: Flat::from_raw(F::ZERO),
172                }
173            }
174            ConstraintExpr::Const(v) => Form {
175                unknowns: Vec::new(),
176                claims: Vec::new(),
177                constant: v.to_hardware(),
178            },
179            ConstraintExpr::Add(a, b) => {
180                let mut acc = forms[a.0 as usize].clone();
181                acc.extend(&forms[b.0 as usize]);
182
183                acc
184            }
185            ConstraintExpr::Scale(coeff, a) => forms[a.0 as usize].scaled(coeff.to_hardware()),
186            ConstraintExpr::Sum(children) => {
187                let mut acc = Form::default();
188                for c in children {
189                    acc.extend(&forms[c.0 as usize]);
190                }
191
192                acc
193            }
194            ConstraintExpr::Mul(a, b) => {
195                let mul = mul_nodes;
196                mul_nodes += 1;
197
198                for (role, side) in [(WireRole::Lhs, a), (WireRole::Rhs, b)] {
199                    let operand = &forms[side.0 as usize];
200
201                    let mut unknowns = operand.unknowns.clone();
202                    unknowns.push((Unknown::Wire { mul, role }, one));
203
204                    affine.push(AffineRow {
205                        unknowns,
206                        claims: operand.claims.clone(),
207                        constant: operand.constant,
208                    });
209                }
210
211                Form {
212                    unknowns: vec![(
213                        Unknown::Wire {
214                            mul,
215                            role: WireRole::Product,
216                        },
217                        one,
218                    )],
219                    claims: Vec::new(),
220                    constant: Flat::from_raw(F::ZERO),
221                }
222            }
223        };
224
225        forms.push(form);
226    }
227
228    let roots = ast
229        .roots
230        .iter()
231        .map(|r| forms[r.0 as usize].clone())
232        .collect();
233
234    PredicateRows {
235        affine,
236        mul_nodes,
237        roots,
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::constraint::ConstraintArena;
245    use hekate_math::{Block128, Flat, HardwareField};
246
247    type F = Block128;
248
249    const LAYOUT: ClaimLayout = ClaimLayout {
250        pad_first: 100,
251        half: 8,
252    };
253
254    const COLS: usize = 6;
255
256    const HONEST: ClaimLayout = ClaimLayout {
257        pad_first: 0,
258        half: COLS as u32,
259    };
260
261    fn mix(seed: u128) -> F {
262        F::from(
263            seed.wrapping_mul(0x9e37_79b9_7f4a_7c15)
264                .wrapping_add(0x51ed_2701),
265        )
266    }
267
268    fn one() -> Flat<F> {
269        Flat::from_raw(F::ONE)
270    }
271
272    fn value_of(
273        unknowns: &[(Unknown, Flat<F>)],
274        claims: &[(u32, Flat<F>)],
275        constant: Flat<F>,
276        masked: &[F],
277        pad: &[F],
278        wires: &[[Flat<F>; 3]],
279    ) -> Flat<F> {
280        let mut acc = constant;
281        for &(idx, coeff) in claims {
282            acc += coeff * masked[idx as usize].to_hardware();
283        }
284
285        for &(unknown, coeff) in unknowns {
286            let v = match unknown {
287                Unknown::Pad(i) => pad[i as usize].to_hardware(),
288                Unknown::Wire { mul, role } => {
289                    let slot = match role {
290                        WireRole::Lhs => 0,
291                        WireRole::Rhs => 1,
292                        WireRole::Product => 2,
293                    };
294
295                    wires[mul as usize][slot]
296                }
297            };
298
299            acc += coeff * v;
300        }
301
302        acc
303    }
304
305    fn ast_of(build: impl FnOnce(&mut ConstraintArena<F>) -> Vec<ExprId>) -> ConstraintAst<F> {
306        let mut arena = ConstraintArena::<F>::new();
307        let roots = build(&mut arena);
308        let labels = roots.iter().map(|_| None).collect();
309
310        ConstraintAst {
311            arena,
312            roots,
313            labels,
314        }
315    }
316
317    fn rich_ast() -> ConstraintAst<F> {
318        ast_of(|a| {
319            let cols: Vec<ExprId> = (0..COLS).map(|i| a.cell(ProgramCell::current(i))).collect();
320            let nexts: Vec<ExprId> = (0..COLS).map(|i| a.cell(ProgramCell::next(i))).collect();
321
322            let shared = a.mul(cols[0], cols[1]);
323            let k = a.constant(mix(7));
324            let scaled = a.scale(mix(11), shared);
325            let summed = a.sum(vec![cols[2], nexts[3], k, scaled]);
326            let deep = a.mul(summed, nexts[4]);
327            let nested = a.mul(deep, cols[5]);
328            let tail = a.add(nested, shared);
329
330            vec![tail, a.add(shared, nexts[0]), deep]
331        })
332    }
333
334    #[test]
335    fn honest_values_satisfy_every_affine_row() {
336        let ast = rich_ast();
337        let rows = compile(&ast, HONEST);
338
339        let plain: Vec<F> = (0..2 * COLS).map(|i| mix(i as u128 + 1)).collect();
340        let pad: Vec<F> = (0..2 * COLS).map(|i| mix(i as u128 + 977)).collect();
341        let masked: Vec<F> = plain.iter().zip(&pad).map(|(c, h)| *c + *h).collect();
342
343        let current: Vec<Flat<F>> = plain[..COLS].iter().map(|v| v.to_hardware()).collect();
344        let next: Vec<Flat<F>> = plain[COLS..].iter().map(|v| v.to_hardware()).collect();
345
346        let consts = ast.precompute_hardware_consts();
347
348        let mut node_values = Vec::new();
349        ast.evaluate_into(&consts, &current, &next, &mut node_values);
350
351        let wires = wire_values(&ast, &node_values);
352
353        assert_eq!(wires.len(), rows.mul_nodes as usize);
354
355        for row in &rows.affine {
356            let zero = Flat::from_raw(F::ZERO);
357            let lhs = value_of(&row.unknowns, &[], zero, &masked, &pad, &wires);
358            let rhs = value_of(&[], &row.claims, row.constant, &masked, &pad, &wires);
359
360            assert_eq!(lhs, rhs);
361        }
362
363        for wire in &wires {
364            assert_eq!(wire[0] * wire[1], wire[2]);
365        }
366    }
367
368    #[test]
369    fn compiled_roots_reproduce_ast_evaluation() {
370        let ast = rich_ast();
371        let rows = compile(&ast, HONEST);
372
373        let plain: Vec<F> = (0..2 * COLS).map(|i| mix(i as u128 + 1)).collect();
374        let pad: Vec<F> = (0..2 * COLS).map(|i| mix(i as u128 + 977)).collect();
375        let masked: Vec<F> = plain.iter().zip(&pad).map(|(c, h)| *c + *h).collect();
376
377        let current: Vec<Flat<F>> = plain[..COLS].iter().map(|v| v.to_hardware()).collect();
378        let next: Vec<Flat<F>> = plain[COLS..].iter().map(|v| v.to_hardware()).collect();
379
380        let consts = ast.precompute_hardware_consts();
381
382        let mut node_values = Vec::new();
383        ast.evaluate_into(&consts, &current, &next, &mut node_values);
384
385        let wires = wire_values(&ast, &node_values);
386        let expected = ast.evaluate(&current, &next);
387
388        for (k, form) in rows.roots.iter().enumerate() {
389            let got = value_of(
390                &form.unknowns,
391                &form.claims,
392                form.constant,
393                &masked,
394                &pad,
395                &wires,
396            );
397
398            assert_eq!(got, expected[k]);
399        }
400    }
401
402    #[test]
403    fn flipped_claim_breaks_roots_that_read_it() {
404        let ast = rich_ast();
405        let rows = compile(&ast, HONEST);
406
407        let plain: Vec<F> = (0..2 * COLS).map(|i| mix(i as u128 + 1)).collect();
408        let pad: Vec<F> = (0..2 * COLS).map(|i| mix(i as u128 + 977)).collect();
409
410        let mut masked: Vec<F> = plain.iter().zip(&pad).map(|(c, h)| *c + *h).collect();
411
412        let current: Vec<Flat<F>> = plain[..COLS].iter().map(|v| v.to_hardware()).collect();
413        let next: Vec<Flat<F>> = plain[COLS..].iter().map(|v| v.to_hardware()).collect();
414
415        let consts = ast.precompute_hardware_consts();
416
417        let mut node_values = Vec::new();
418        ast.evaluate_into(&consts, &current, &next, &mut node_values);
419
420        let wires = wire_values(&ast, &node_values);
421
422        let honest: Vec<Flat<F>> = rows
423            .roots
424            .iter()
425            .map(|f| value_of(&f.unknowns, &f.claims, f.constant, &masked, &pad, &wires))
426            .collect();
427
428        const FLIPPED: u32 = 2;
429        masked[FLIPPED as usize] += F::ONE;
430
431        for (k, form) in rows.roots.iter().enumerate() {
432            let got = value_of(
433                &form.unknowns,
434                &form.claims,
435                form.constant,
436                &masked,
437                &pad,
438                &wires,
439            );
440
441            let reads_it = form.claims.iter().any(|&(idx, _)| idx == FLIPPED);
442
443            assert_eq!(got != honest[k], reads_it);
444        }
445    }
446
447    #[test]
448    fn cell_binds_one_pad_entry_and_one_claim() {
449        let ast = ast_of(|a| vec![a.cell(ProgramCell::current(3))]);
450        let rows = compile(&ast, LAYOUT);
451
452        assert_eq!(rows.mul_nodes, 0);
453        assert!(rows.affine.is_empty());
454        assert_eq!(rows.roots[0].unknowns, vec![(Unknown::Pad(103), one())]);
455        assert_eq!(rows.roots[0].claims, vec![(3, one())]);
456    }
457
458    #[test]
459    fn next_row_cell_lands_in_second_half() {
460        let ast = ast_of(|a| vec![a.cell(ProgramCell::next(3))]);
461        let rows = compile(&ast, LAYOUT);
462
463        assert_eq!(rows.roots[0].claims, vec![(11, one())]);
464        assert_eq!(rows.roots[0].unknowns, vec![(Unknown::Pad(111), one())]);
465    }
466
467    #[test]
468    fn mul_emits_two_affine_rows_and_yields_its_product_wire() {
469        let ast = ast_of(|a| {
470            let x = a.cell(ProgramCell::current(0));
471            let y = a.cell(ProgramCell::current(1));
472
473            vec![a.mul(x, y)]
474        });
475
476        let rows = compile(&ast, LAYOUT);
477
478        assert_eq!(rows.mul_nodes, 1);
479        assert_eq!(rows.affine.len(), 2);
480        assert_eq!(
481            rows.roots[0].unknowns,
482            vec![(
483                Unknown::Wire {
484                    mul: 0,
485                    role: WireRole::Product
486                },
487                one()
488            )]
489        );
490    }
491
492    #[test]
493    fn wire_row_pins_its_operand() {
494        let ast = ast_of(|a| {
495            let x = a.cell(ProgramCell::current(0));
496            let y = a.cell(ProgramCell::current(1));
497
498            vec![a.mul(x, y)]
499        });
500
501        let rows = compile(&ast, LAYOUT);
502        let lhs = &rows.affine[0];
503
504        assert_eq!(lhs.claims, vec![(0, one())]);
505        assert!(lhs.unknowns.contains(&(Unknown::Pad(100), one())));
506        assert!(lhs.unknowns.contains(&(
507            Unknown::Wire {
508                mul: 0,
509                role: WireRole::Lhs
510            },
511            one()
512        )));
513    }
514
515    #[test]
516    fn nested_muls_number_their_wires_in_arena_order() {
517        let ast = ast_of(|a| {
518            let x = a.cell(ProgramCell::current(0));
519            let y = a.cell(ProgramCell::current(1));
520            let z = a.cell(ProgramCell::current(2));
521            let inner = a.mul(x, y);
522
523            vec![a.mul(inner, z)]
524        });
525
526        let rows = compile(&ast, LAYOUT);
527
528        assert_eq!(rows.mul_nodes, 2);
529        assert_eq!(rows.affine.len(), 4);
530        assert_eq!(
531            rows.affine[2].unknowns,
532            vec![
533                (
534                    Unknown::Wire {
535                        mul: 0,
536                        role: WireRole::Product
537                    },
538                    one()
539                ),
540                (
541                    Unknown::Wire {
542                        mul: 1,
543                        role: WireRole::Lhs
544                    },
545                    one()
546                )
547            ]
548        );
549    }
550
551    #[test]
552    fn shared_mul_is_compiled_once() {
553        let ast = ast_of(|a| {
554            let x = a.cell(ProgramCell::current(0));
555            let y = a.cell(ProgramCell::current(1));
556            let shared = a.mul(x, y);
557
558            vec![a.add(shared, x), a.add(shared, y)]
559        });
560
561        let rows = compile(&ast, LAYOUT);
562
563        assert_eq!(rows.mul_nodes, 1);
564        assert_eq!(rows.affine.len(), 2);
565    }
566
567    #[test]
568    fn constant_lands_on_public_side() {
569        let ast = ast_of(|a| {
570            let k = a.constant(F::ONE);
571            let x = a.cell(ProgramCell::current(0));
572
573            vec![a.add(k, x)]
574        });
575
576        let rows = compile(&ast, LAYOUT);
577
578        assert_eq!(rows.roots[0].constant, one());
579        assert_eq!(rows.roots[0].claims, vec![(0, one())]);
580    }
581
582    #[test]
583    fn scale_multiplies_every_coefficient() {
584        let two = F::from(2u8);
585
586        let ast = ast_of(|a| {
587            let x = a.cell(ProgramCell::current(0));
588            let k = a.constant(F::ONE);
589            let s = a.add(x, k);
590
591            vec![a.scale(two, s)]
592        });
593
594        let rows = compile(&ast, LAYOUT);
595
596        assert_eq!(rows.roots[0].claims, vec![(0, two.to_hardware())]);
597        assert_eq!(rows.roots[0].constant, two.to_hardware());
598    }
599
600    #[test]
601    fn sum_collects_every_child() {
602        let ast = ast_of(|a| {
603            let cells: Vec<ExprId> = (0..4).map(|i| a.cell(ProgramCell::current(i))).collect();
604
605            vec![a.sum(cells)]
606        });
607
608        let rows = compile(&ast, LAYOUT);
609
610        assert_eq!(rows.roots[0].claims.len(), 4);
611        assert_eq!(rows.roots[0].unknowns.len(), 4);
612    }
613
614    #[test]
615    fn mul_count_agrees_with_shape_pass() {
616        let ast = ast_of(|a| {
617            let x = a.cell(ProgramCell::current(0));
618            let y = a.cell(ProgramCell::current(1));
619            let z = a.cell(ProgramCell::current(2));
620            let p = a.mul(x, y);
621            let q = a.mul(p, z);
622
623            vec![a.add(p, q)]
624        });
625
626        let rows = compile(&ast, LAYOUT);
627
628        assert_eq!(rows.mul_nodes as usize, crate::outer::mul_node_count(&ast));
629    }
630}