Skip to main content

hekate_program/constraint/
builder.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
5//! Constraint Builder DSL.
6//!
7//! Provides `ConstraintSystem` and `Expr` for writing
8//! algebraic constraints with operator overloading.
9//!
10//! # Example
11//!
12//! ```ignore
13//! let cs = ConstraintSystem::<F>::new();
14//! let [a, b, q] = [cs.col(0), cs.col(1), cs.col(2)];
15//! let [na, nb] = [cs.next(0), cs.next(1)];
16//!
17//! cs.constrain(q * (na + b));       // next_a = b
18//! cs.constrain(q * (nb + a + b));   // next_b = a + b
19//!
20//! let ast = cs.build();
21//! ```
22//!
23//! `Expr` is `Copy` — same expression reused in
24//! multiple constraints shares the underlying DAG node.
25//! Cell references are auto-deduplicated.
26//!
27//! `Sub` delegates to `Add` because in GF(2^k),
28//! subtraction is addition (XOR). This is correct
29//! for binary tower fields only.
30
31use crate::ProgramCell;
32use crate::constraint::{ConstraintArena, ConstraintAst, ExprId};
33use alloc::vec::Vec;
34use core::cell::RefCell;
35use core::ops::{Add, Mul, Sub};
36use hekate_math::TowerField;
37
38// =================================================================
39// CONSTRAINT SYSTEM
40// =================================================================
41
42/// Builder context for algebraic constraints.
43pub struct ConstraintSystem<F: TowerField> {
44    inner: RefCell<Inner<F>>,
45}
46
47struct Inner<F: TowerField> {
48    arena: ConstraintArena<F>,
49    roots: Vec<ExprId>,
50    labels: Vec<Option<&'static str>>,
51}
52
53impl<F: TowerField> ConstraintSystem<F> {
54    /// Create a new constraint system.
55    pub fn new() -> Self {
56        Self {
57            inner: RefCell::new(Inner {
58                arena: ConstraintArena::new(),
59                roots: Vec::new(),
60                labels: Vec::new(),
61            }),
62        }
63    }
64
65    /// Create a builder from an existing AST.
66    pub fn from_ast(ast: ConstraintAst<F>) -> Self {
67        Self {
68            inner: RefCell::new(Inner {
69                arena: ast.arena,
70                roots: ast.roots,
71                labels: ast.labels,
72            }),
73        }
74    }
75
76    // ===========================================================
77    // Cell References
78    // ===========================================================
79
80    /// Reference to a column in the current row.
81    pub fn col(&self, idx: usize) -> Expr<'_, F> {
82        let id = self
83            .inner
84            .borrow_mut()
85            .arena
86            .cell(ProgramCell::current(idx));
87        Expr { id, cs: self }
88    }
89
90    /// Reference to a column in the next row.
91    pub fn next(&self, idx: usize) -> Expr<'_, F> {
92        let id = self.inner.borrow_mut().arena.cell(ProgramCell::next(idx));
93        Expr { id, cs: self }
94    }
95
96    // ===========================================================
97    // Constants
98    // ===========================================================
99
100    /// Field constant.
101    pub fn constant(&self, val: F) -> Expr<'_, F> {
102        let id = self.inner.borrow_mut().arena.constant(val);
103        Expr { id, cs: self }
104    }
105
106    /// The multiplicative identity.
107    pub fn one(&self) -> Expr<'_, F> {
108        self.constant(F::ONE)
109    }
110
111    // ===========================================================
112    // Arithmetic
113    // ===========================================================
114
115    /// Scalar multiplication:
116    /// `coeff * expr`.
117    ///
118    /// Use this for powers of 2, coefficients, etc.
119    /// Orphan rules prevent implementing `F * Expr`
120    /// via operator overloading.
121    pub fn scale(&self, coeff: F, expr: Expr<'_, F>) -> Expr<'_, F> {
122        let id = self.inner.borrow_mut().arena.scale(coeff, expr.id);
123        Expr { id, cs: self }
124    }
125
126    /// N-ary sum. More efficient than chaining binary `+`
127    /// for linear combinations (avoids deep Add chains).
128    pub fn sum(&self, children: &[Expr<'_, F>]) -> Expr<'_, F> {
129        let ids: Vec<ExprId> = children.iter().map(|e| e.id).collect();
130        let id = self.inner.borrow_mut().arena.sum(ids);
131
132        Expr { id, cs: self }
133    }
134
135    // ===========================================================
136    // Constraint Registration
137    // ===========================================================
138
139    /// Register a constraint:
140    /// `expr = 0`.
141    ///
142    /// The expression must evaluate to zero
143    /// for every valid row in the execution trace.
144    pub fn constrain(&self, expr: Expr<'_, F>) {
145        let mut inner = self.inner.borrow_mut();
146        inner.roots.push(expr.id);
147        inner.labels.push(None);
148    }
149
150    pub fn constrain_named(&self, label: &'static str, expr: Expr<'_, F>) {
151        let mut inner = self.inner.borrow_mut();
152        inner.roots.push(expr.id);
153        inner.labels.push(Some(label));
154    }
155
156    // ===========================================================
157    // Built-in Gadgets
158    // ===========================================================
159
160    /// Assert that `s` is boolean:
161    /// `s * (s + 1) = 0`.
162    ///
163    /// In GF(2^k), `s + 1` equals `s - 1`.
164    /// Enforces `s ∈ {0, 1}`.
165    pub fn assert_boolean(&self, s: Expr<'_, F>) {
166        // s * s + s = 0  (expanded form of s*(s+1)=0)
167        let sq = s * s;
168        let expr = sq + s;
169
170        self.constrain_named("boolean", expr);
171    }
172
173    /// Assert that `body = 0` whenever `sel = 1`.
174    ///
175    /// Registers `sel * body = 0`.
176    /// When `sel = 0`, the constraint
177    /// is trivially satisfied.
178    pub fn assert_zero_when(&self, sel: Expr<'_, F>, body: Expr<'_, F>) {
179        self.constrain_named("zero_when", sel * body);
180    }
181
182    /// Assert that exactly
183    /// one selector is active.
184    /// Enforces:
185    /// sum(selectors) = 1.
186    ///
187    /// In GF(2^k):
188    /// sum(s_i) + 1 = 0.
189    pub fn assert_one_hot(&self, selectors: &[Expr<'_, F>]) {
190        let s = self.sum(selectors);
191        let one = self.one();
192
193        self.constrain_named("one_hot", s + one);
194    }
195
196    /// Roots registered so far.
197    pub fn num_roots(&self) -> usize {
198        self.inner.borrow().roots.len()
199    }
200
201    /// Append another AST's nodes and roots,
202    /// preserving this builder's root order.
203    pub fn merge_ast(&self, other: ConstraintAst<F>) {
204        let mut inner = self.inner.borrow_mut();
205        let mut ast = ConstraintAst {
206            arena: core::mem::take(&mut inner.arena),
207            roots: core::mem::take(&mut inner.roots),
208            labels: core::mem::take(&mut inner.labels),
209        };
210
211        ast.merge(other);
212
213        inner.arena = ast.arena;
214        inner.roots = ast.roots;
215        inner.labels = ast.labels;
216    }
217
218    // ===========================================================
219    // Compile
220    // ===========================================================
221
222    /// Consume the builder and
223    /// produce a `ConstraintAst`.
224    ///
225    /// All `Expr` handles must be
226    /// dropped before calling this
227    /// (enforced by the borrow checker,
228    /// `build` takes `self`).
229    pub fn build(self) -> ConstraintAst<F> {
230        let inner = self.inner.into_inner();
231        ConstraintAst {
232            arena: inner.arena,
233            roots: inner.roots,
234            labels: inner.labels,
235        }
236    }
237}
238
239impl<F: TowerField> Default for ConstraintSystem<F> {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245// =================================================================
246// EXPRESSION HANDLE
247// =================================================================
248
249/// Lightweight handle to a DAG node in a `ConstraintSystem`.
250///
251/// Supports `+`, `*`, `-` via operator overloading.
252/// `Sub` delegates to `Add` (correct for GF(2^k) only).
253#[derive(Clone, Copy)]
254pub struct Expr<'a, F: TowerField> {
255    pub(crate) id: ExprId,
256    pub(crate) cs: &'a ConstraintSystem<F>,
257}
258
259// a + b
260impl<'a, F: TowerField> Add for Expr<'a, F> {
261    type Output = Expr<'a, F>;
262
263    fn add(self, rhs: Self) -> Self::Output {
264        let id = self.cs.inner.borrow_mut().arena.add(self.id, rhs.id);
265        Expr { id, cs: self.cs }
266    }
267}
268
269// a * b
270impl<'a, F: TowerField> Mul for Expr<'a, F> {
271    type Output = Expr<'a, F>;
272
273    fn mul(self, rhs: Self) -> Self::Output {
274        let id = self.cs.inner.borrow_mut().arena.mul(self.id, rhs.id);
275        Expr { id, cs: self.cs }
276    }
277}
278
279// a - b  (in GF(2^k), subtraction = addition)
280impl<'a, F: TowerField> Sub for Expr<'a, F> {
281    type Output = Expr<'a, F>;
282
283    fn sub(self, rhs: Self) -> Self::Output {
284        // In characteristic 2:
285        // -1 = 1, so a - b = a + b = a XOR b.
286        // This is correct for binary tower fields only.
287        let id = self.cs.inner.borrow_mut().arena.add(self.id, rhs.id);
288        Expr { id, cs: self.cs }
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use crate::constraint::ConstraintExpr;
296    use hekate_math::Block128;
297
298    type F = Block128;
299
300    #[test]
301    fn basic_fibonacci_builder() {
302        let cs = ConstraintSystem::<F>::new();
303
304        let a = cs.col(0);
305        let b = cs.col(1);
306        let q = cs.col(2);
307        let na = cs.next(0);
308        let nb = cs.next(1);
309
310        // q * (next_a + b) = 0
311        cs.constrain(q * (na + b));
312        // q * (next_b + a + b) = 0
313        cs.constrain(q * (nb + a + b));
314
315        let ast = cs.build();
316
317        assert_eq!(ast.roots.len(), 2);
318        assert!(!ast.arena.is_empty());
319
320        // Both roots should be Mul (selector * sum)
321        for &root in &ast.roots {
322            match ast.arena.get(root) {
323                ConstraintExpr::Mul(_, _) => {}
324                other => panic!("Expected Mul root, got {:?}", other),
325            }
326        }
327    }
328
329    #[test]
330    fn cell_dedup_through_builder() {
331        let cs = ConstraintSystem::<F>::new();
332
333        let a1 = cs.col(0);
334        let a2 = cs.col(0);
335        let b = cs.col(1);
336
337        // Same column → same ExprId
338        assert_eq!(a1.id, a2.id);
339        // Different column → different ExprId
340        assert_ne!(a1.id, b.id);
341    }
342
343    #[test]
344    fn sub_equals_add_in_char2() {
345        let cs = ConstraintSystem::<F>::new();
346
347        let a = cs.col(0);
348        let b = cs.col(1);
349
350        let sum = a + b;
351        let diff = a - b;
352
353        // In GF(2^k), a + b == a - b
354        // Both should produce Add nodes with same children
355        let ast_sum = cs.inner.borrow();
356        match (ast_sum.arena.get(sum.id), ast_sum.arena.get(diff.id)) {
357            (ConstraintExpr::Add(la, ra), ConstraintExpr::Add(lb, rb)) => {
358                assert_eq!(la, lb);
359                assert_eq!(ra, rb);
360            }
361            _ => panic!("Expected Add nodes for both + and -"),
362        }
363    }
364
365    #[test]
366    fn assert_boolean_structure() {
367        let cs = ConstraintSystem::<F>::new();
368        let s = cs.col(5);
369
370        cs.assert_boolean(s);
371
372        let ast = cs.build();
373        assert_eq!(ast.roots.len(), 1);
374
375        // Root should be Add(Mul(s,s), s) = s² + s
376        match ast.arena.get(ast.roots[0]) {
377            ConstraintExpr::Add(lhs, rhs) => {
378                // lhs = s * s
379                match ast.arena.get(*lhs) {
380                    ConstraintExpr::Mul(a, b) => {
381                        assert_eq!(a, b); // same cell
382                    }
383                    other => panic!("Expected Mul for s², got {:?}", other),
384                }
385
386                // rhs = s
387                match ast.arena.get(*rhs) {
388                    ConstraintExpr::Cell(cell) => {
389                        assert_eq!(cell.col_idx, 5);
390                        assert!(!cell.next_row);
391                    }
392                    other => panic!("Expected Cell for s, got {:?}", other),
393                }
394            }
395            other => panic!("Expected Add for s²+s, got {:?}", other),
396        }
397    }
398
399    #[test]
400    fn assert_zero_when_structure() {
401        let cs = ConstraintSystem::<F>::new();
402        let sel = cs.col(0);
403        let body = cs.col(1) + cs.col(2);
404
405        cs.assert_zero_when(sel, body);
406
407        let ast = cs.build();
408        assert_eq!(ast.roots.len(), 1);
409
410        // Root = Mul(sel, Add(col1, col2))
411        match ast.arena.get(ast.roots[0]) {
412            ConstraintExpr::Mul(_, _) => {}
413            other => panic!("Expected Mul, got {:?}", other),
414        }
415    }
416
417    #[test]
418    fn scale_produces_scale_node() {
419        let cs = ConstraintSystem::<F>::new();
420        let a = cs.col(0);
421        let scaled = cs.scale(F::from(8u128), a);
422
423        // Save ids before build() consumes cs
424        let a_id = a.id;
425        let scaled_id = scaled.id;
426
427        let ast = cs.build();
428        match ast.arena.get(scaled_id) {
429            ConstraintExpr::Scale(coeff, inner) => {
430                assert_eq!(*coeff, F::from(8u128));
431                assert_eq!(*inner, a_id);
432            }
433            other => panic!("Expected Scale, got {:?}", other),
434        }
435    }
436
437    #[test]
438    fn sum_produces_sum_node() {
439        let cs = ConstraintSystem::<F>::new();
440        let a = cs.col(0);
441        let b = cs.col(1);
442        let c = cs.col(2);
443        let s = cs.sum(&[a, b, c]);
444
445        // Save ids before build() consumes cs
446        let (a_id, b_id, c_id) = (a.id, b.id, c.id);
447
448        let s_id = s.id;
449        let ast = cs.build();
450
451        match ast.arena.get(s_id) {
452            ConstraintExpr::Sum(children) => {
453                assert_eq!(children.len(), 3);
454                assert_eq!(children[0], a_id);
455                assert_eq!(children[1], b_id);
456                assert_eq!(children[2], c_id);
457            }
458            other => panic!("Expected Sum, got {:?}", other),
459        }
460    }
461
462    #[test]
463    fn dag_sharing_via_expr_reuse() {
464        let cs = ConstraintSystem::<F>::new();
465
466        let a = cs.col(0);
467        let b = cs.col(1);
468        let c = cs.col(2);
469
470        // Shared sub-expression
471        let theta = cs.sum(&[a, b, c]);
472
473        // Use theta in two constraints
474        let d = cs.col(3);
475        cs.constrain(theta * d);
476        cs.constrain(theta * a);
477
478        let ast = cs.build();
479        assert_eq!(ast.roots.len(), 2);
480
481        // Both roots reference the same theta node
482        match (ast.arena.get(ast.roots[0]), ast.arena.get(ast.roots[1])) {
483            (ConstraintExpr::Mul(lhs0, _), ConstraintExpr::Mul(lhs1, _)) => {
484                assert_eq!(lhs0, lhs1); // same theta ExprId
485            }
486            _ => panic!("Expected Mul roots"),
487        }
488    }
489
490    #[test]
491    fn empty_system_produces_empty_ast() {
492        let cs = ConstraintSystem::<F>::new();
493        let ast = cs.build();
494        assert!(ast.roots.is_empty());
495        assert!(ast.arena.is_empty());
496    }
497
498    #[test]
499    fn builder_matches_manual_structure() {
500        // Build via builder: q * (next_a + b)
501        let cs = ConstraintSystem::<F>::new();
502        let _a = cs.col(0);
503        let b = cs.col(1);
504        let q = cs.col(2);
505        let na = cs.next(0);
506
507        cs.constrain(q * (na + b));
508
509        let ast = cs.build();
510
511        // Verify: Mul(Cell(2,curr), Add(Cell(0,next), Cell(1,curr)))
512        assert_eq!(ast.roots.len(), 1);
513
514        match ast.arena.get(ast.roots[0]) {
515            ConstraintExpr::Mul(lhs, rhs) => {
516                match ast.arena.get(*lhs) {
517                    ConstraintExpr::Cell(cell) => {
518                        assert_eq!(cell.col_idx, 2);
519                        assert!(!cell.next_row);
520                    }
521                    other => panic!("Expected Cell for q, got {:?}", other),
522                }
523                match ast.arena.get(*rhs) {
524                    ConstraintExpr::Add(a, b) => {
525                        match ast.arena.get(*a) {
526                            ConstraintExpr::Cell(cell) => {
527                                assert_eq!(cell.col_idx, 0);
528                                assert!(cell.next_row);
529                            }
530                            other => panic!("Expected Cell for next_a, got {:?}", other),
531                        }
532                        match ast.arena.get(*b) {
533                            ConstraintExpr::Cell(cell) => {
534                                assert_eq!(cell.col_idx, 1);
535                                assert!(!cell.next_row);
536                            }
537                            other => panic!("Expected Cell for b, got {:?}", other),
538                        }
539                    }
540                    other => panic!("Expected Add, got {:?}", other),
541                }
542            }
543            other => panic!("Expected Mul root, got {:?}", other),
544        }
545    }
546
547    #[test]
548    fn labels_round_trip_through_build() {
549        let cs = ConstraintSystem::<F>::new();
550        let a = cs.col(0);
551        let b = cs.col(1);
552
553        cs.constrain(a + b);
554        cs.constrain_named("transition", a * b);
555        cs.assert_boolean(a);
556
557        let ast = cs.build();
558
559        assert_eq!(ast.roots.len(), 3);
560        assert_eq!(ast.labels.len(), 3);
561        assert_eq!(ast.labels[0], None);
562        assert_eq!(ast.labels[1], Some("transition"));
563        assert_eq!(ast.labels[2], Some("boolean"));
564    }
565
566    #[test]
567    fn labels_preserved_through_merge() {
568        let cs1 = ConstraintSystem::<F>::new();
569        cs1.constrain_named("first", cs1.col(0));
570
571        let mut ast1 = cs1.build();
572
573        let cs2 = ConstraintSystem::<F>::new();
574
575        cs2.constrain(cs2.col(0));
576        cs2.constrain_named("second", cs2.col(1));
577
578        let ast2 = cs2.build();
579
580        ast1.merge(ast2);
581
582        assert_eq!(ast1.roots.len(), 3);
583        assert_eq!(ast1.labels.len(), 3);
584        assert_eq!(ast1.labels[0], Some("first"));
585        assert_eq!(ast1.labels[1], None);
586        assert_eq!(ast1.labels[2], Some("second"));
587    }
588
589    #[test]
590    fn labels_preserved_through_from_ast() {
591        let cs = ConstraintSystem::<F>::new();
592        cs.constrain_named("original", cs.col(0));
593
594        let ast = cs.build();
595
596        let cs2 = ConstraintSystem::from_ast(ast);
597        cs2.constrain_named("added", cs2.col(1));
598
599        let ast2 = cs2.build();
600
601        assert_eq!(ast2.labels.len(), 2);
602        assert_eq!(ast2.labels[0], Some("original"));
603        assert_eq!(ast2.labels[1], Some("added"));
604    }
605
606    #[test]
607    fn builtin_gadgets_have_labels() {
608        let cs = ConstraintSystem::<F>::new();
609
610        let a = cs.col(0);
611        let b = cs.col(1);
612
613        cs.assert_boolean(a);
614        cs.assert_zero_when(a, b);
615        cs.assert_one_hot(&[a, b]);
616
617        let ast = cs.build();
618
619        assert_eq!(ast.labels.len(), 3);
620        assert_eq!(ast.labels[0], Some("boolean"));
621        assert_eq!(ast.labels[1], Some("zero_when"));
622        assert_eq!(ast.labels[2], Some("one_hot"));
623    }
624}