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