1use crate::ProgramCell;
8use alloc::vec;
9use alloc::vec::Vec;
10use hashbrown::HashMap;
11use hekate_core::errors::Error;
12use hekate_math::{Flat, HardwareField, TowerField};
13
14pub mod builder;
15
16#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct ConstraintTerm<F> {
21 pub coeff: F,
22 pub poly_ind: Vec<ProgramCell>, }
24
25impl<F: TowerField> ConstraintTerm<F> {
26 pub fn new(coeff: F, cells: Vec<ProgramCell>) -> Self {
27 Self {
28 coeff,
29 poly_ind: cells,
30 }
31 }
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct Constraint<F> {
38 pub terms: Vec<ConstraintTerm<F>>,
39}
40
41impl<F: TowerField> Constraint<F> {
42 pub fn new(terms: Vec<ConstraintTerm<F>>) -> Self {
43 Self { terms }
44 }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
50pub enum BoundaryTarget<F> {
51 PublicInput(usize),
54
55 Constant(F),
58}
59
60#[derive(Clone, Debug)]
63pub struct BoundaryConstraint<F> {
64 pub col_idx: usize,
65 pub row_idx: usize,
66 pub target: BoundaryTarget<F>,
67}
68
69impl<F> BoundaryConstraint<F> {
70 pub fn with_constant(col_idx: usize, row_idx: usize, val: F) -> Self {
71 Self {
72 col_idx,
73 row_idx,
74 target: BoundaryTarget::Constant(val),
75 }
76 }
77
78 pub fn with_public_input(col_idx: usize, row_idx: usize, input_idx: usize) -> Self {
79 Self {
80 col_idx,
81 row_idx,
82 target: BoundaryTarget::PublicInput(input_idx),
83 }
84 }
85}
86
87impl<F: TowerField> BoundaryConstraint<F> {
88 pub fn resolve_target(
93 &self,
94 instance: &crate::ProgramInstance<F>,
95 ) -> hekate_core::errors::Result<F> {
96 match &self.target {
97 BoundaryTarget::Constant(v) => Ok(*v),
98 BoundaryTarget::PublicInput(idx) => {
99 instance.public_input(*idx).ok_or(Error::Protocol {
100 protocol: "boundary",
101 message: "public_input_idx out of bounds",
102 })
103 }
104 }
105 }
106
107 pub fn absorb_into<H: hekate_crypto::Hasher>(
112 &self,
113 transcript: &mut hekate_crypto::transcript::Transcript<H>,
114 ) {
115 transcript.append_u64(b"chiplet_bnd_col", self.col_idx as u64);
116 transcript.append_u64(b"chiplet_bnd_row", self.row_idx as u64);
117
118 match &self.target {
119 BoundaryTarget::PublicInput(idx) => {
120 transcript.append_u64(b"chiplet_bnd_kind", 0);
121 transcript.append_u64(b"chiplet_bnd_pub", *idx as u64);
122 }
123 BoundaryTarget::Constant(v) => {
124 transcript.append_u64(b"chiplet_bnd_kind", 1);
125 transcript.append_field(b"chiplet_bnd_const", *v);
126 }
127 }
128 }
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
137pub struct ExprId(pub u32);
138
139#[derive(Clone, Debug)]
142pub enum ConstraintExpr<F> {
143 Cell(ProgramCell),
145
146 Const(F),
148
149 Add(ExprId, ExprId),
152
153 Mul(ExprId, ExprId),
155
156 Scale(F, ExprId),
162
163 Sum(Vec<ExprId>),
168}
169
170pub struct ConstraintArena<F> {
181 nodes: Vec<ConstraintExpr<F>>,
182
183 cell_cache: HashMap<ProgramCell, ExprId>,
186}
187
188impl<F: TowerField> Default for ConstraintArena<F> {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194impl<F: TowerField> ConstraintArena<F> {
195 pub fn new() -> Self {
196 Self {
197 nodes: Vec::new(),
198 cell_cache: HashMap::new(),
199 }
200 }
201
202 pub fn alloc(&mut self, expr: ConstraintExpr<F>) -> ExprId {
205 let id = ExprId(self.nodes.len() as u32);
206 self.nodes.push(expr);
207
208 id
209 }
210
211 pub fn get(&self, id: ExprId) -> &ConstraintExpr<F> {
213 &self.nodes[id.0 as usize]
214 }
215
216 pub fn len(&self) -> usize {
218 self.nodes.len()
219 }
220
221 pub fn is_empty(&self) -> bool {
222 self.nodes.is_empty()
223 }
224
225 pub fn shift_cells(&mut self, offset: usize) {
230 for node in &mut self.nodes {
231 if let ConstraintExpr::Cell(cell) = node {
232 cell.col_idx += offset;
233 }
234 }
235
236 let old_cache = core::mem::take(&mut self.cell_cache);
237 for (mut cell, id) in old_cache {
238 cell.col_idx += offset;
239 self.cell_cache.insert(cell, id);
240 }
241 }
242
243 pub fn cell(&mut self, cell: ProgramCell) -> ExprId {
247 if let Some(&id) = self.cell_cache.get(&cell) {
248 return id;
249 }
250
251 let id = self.alloc(ConstraintExpr::Cell(cell));
252 self.cell_cache.insert(cell, id);
253
254 id
255 }
256
257 pub fn constant(&mut self, val: F) -> ExprId {
259 self.alloc(ConstraintExpr::Const(val))
260 }
261
262 pub fn add(&mut self, a: ExprId, b: ExprId) -> ExprId {
264 self.alloc(ConstraintExpr::Add(a, b))
265 }
266
267 pub fn mul(&mut self, a: ExprId, b: ExprId) -> ExprId {
269 self.alloc(ConstraintExpr::Mul(a, b))
270 }
271
272 pub fn scale(&mut self, coeff: F, a: ExprId) -> ExprId {
274 self.alloc(ConstraintExpr::Scale(coeff, a))
275 }
276
277 pub fn sum(&mut self, children: Vec<ExprId>) -> ExprId {
279 self.alloc(ConstraintExpr::Sum(children))
280 }
281}
282
283pub struct ConstraintAst<F> {
287 pub arena: ConstraintArena<F>,
288 pub roots: Vec<ExprId>,
289 pub labels: Vec<Option<&'static str>>,
290}
291
292impl<F: TowerField> Clone for ConstraintArena<F> {
293 fn clone(&self) -> Self {
294 Self {
295 nodes: self.nodes.clone(),
296 cell_cache: self.cell_cache.clone(),
297 }
298 }
299}
300
301impl<F: TowerField> Clone for ConstraintAst<F> {
302 fn clone(&self) -> Self {
303 Self {
304 arena: self.arena.clone(),
305 roots: self.roots.clone(),
306 labels: self.labels.clone(),
307 }
308 }
309}
310
311impl<F: TowerField> ConstraintAst<F> {
312 pub fn max_degree(&self) -> usize {
315 if self.arena.is_empty() {
316 return 0;
317 }
318
319 let n = self.arena.len();
320 let mut deg: Vec<usize> = Vec::with_capacity(n);
321
322 for i in 0..n {
323 let d = match self.arena.get(ExprId(i as u32)) {
324 ConstraintExpr::Cell(_) => 1,
325 ConstraintExpr::Const(_) => 0,
326 ConstraintExpr::Add(a, b) => deg[a.0 as usize].max(deg[b.0 as usize]),
327 ConstraintExpr::Mul(a, b) => deg[a.0 as usize] + deg[b.0 as usize],
328 ConstraintExpr::Scale(_, a) => deg[a.0 as usize],
329 ConstraintExpr::Sum(children) => children
330 .iter()
331 .map(|c| deg[c.0 as usize])
332 .max()
333 .unwrap_or(0),
334 };
335
336 deg.push(d);
337 }
338
339 self.roots
340 .iter()
341 .map(|r| deg[r.0 as usize])
342 .max()
343 .unwrap_or(0)
344 }
345
346 pub fn precompute_hardware_consts(&self) -> Vec<Flat<F>>
349 where
350 F: HardwareField,
351 {
352 let n = self.arena.len();
353
354 let mut consts: Vec<Flat<F>> = Vec::with_capacity(n);
355 for i in 0..n {
356 let c = match self.arena.get(ExprId(i as u32)) {
357 ConstraintExpr::Const(k) => k.to_hardware(),
358 ConstraintExpr::Scale(k, _) => k.to_hardware(),
359 _ => Flat::from_raw(F::ZERO),
360 };
361
362 consts.push(c);
363 }
364
365 consts
366 }
367
368 pub fn evaluate(&self, current_row: &[Flat<F>], next_row: &[Flat<F>]) -> Vec<Flat<F>>
371 where
372 F: HardwareField,
373 {
374 let consts = self.precompute_hardware_consts();
375 let mut buf: Vec<Flat<F>> = Vec::with_capacity(self.arena.len());
376
377 self.evaluate_into(&consts, current_row, next_row, &mut buf);
378
379 self.roots.iter().map(|r| buf[r.0 as usize]).collect()
380 }
381
382 pub fn evaluate_into(
385 &self,
386 consts: &[Flat<F>],
387 current_row: &[Flat<F>],
388 next_row: &[Flat<F>],
389 buf: &mut Vec<Flat<F>>,
390 ) where
391 F: HardwareField,
392 {
393 buf.clear();
394
395 for (i, &coeff) in consts.iter().enumerate() {
396 let v = match self.arena.get(ExprId(i as u32)) {
397 ConstraintExpr::Cell(cell) => {
398 if cell.next_row {
399 next_row[cell.col_idx]
400 } else {
401 current_row[cell.col_idx]
402 }
403 }
404 ConstraintExpr::Const(_) => coeff,
405 ConstraintExpr::Add(a, b) => buf[a.0 as usize] + buf[b.0 as usize],
406 ConstraintExpr::Mul(a, b) => buf[a.0 as usize] * buf[b.0 as usize],
407 ConstraintExpr::Scale(_, a) => coeff * buf[a.0 as usize],
408 ConstraintExpr::Sum(children) => {
409 let mut s = Flat::from_raw(F::ZERO);
410 for c in children {
411 s += buf[c.0 as usize];
412 }
413
414 s
415 }
416 };
417
418 buf.push(v);
419 }
420 }
421
422 pub fn merge(&mut self, other: ConstraintAst<F>) {
424 let mut id_map: Vec<ExprId> = Vec::with_capacity(other.arena.len());
425 for node in other.arena.nodes {
426 let new_id = match node {
427 ConstraintExpr::Cell(cell) => self.arena.cell(cell),
428 ConstraintExpr::Const(val) => self.arena.constant(val),
429 ConstraintExpr::Add(a, b) => {
430 self.arena.add(id_map[a.0 as usize], id_map[b.0 as usize])
431 }
432 ConstraintExpr::Mul(a, b) => {
433 self.arena.mul(id_map[a.0 as usize], id_map[b.0 as usize])
434 }
435 ConstraintExpr::Scale(coeff, inner) => {
436 self.arena.scale(coeff, id_map[inner.0 as usize])
437 }
438 ConstraintExpr::Sum(children) => {
439 let remapped: Vec<ExprId> =
440 children.into_iter().map(|c| id_map[c.0 as usize]).collect();
441 self.arena.sum(remapped)
442 }
443 };
444
445 id_map.push(new_id);
446 }
447
448 for (root, label) in other.roots.into_iter().zip(other.labels) {
449 self.roots.push(id_map[root.0 as usize]);
450 self.labels.push(label);
451 }
452 }
453
454 pub fn to_constraints(&self) -> Vec<Constraint<F>> {
457 type FlatTerm<F> = (F, Vec<ProgramCell>);
460
461 fn expand<F: TowerField>(
462 arena: &ConstraintArena<F>,
463 id: ExprId,
464 cache: &mut Vec<Option<Vec<FlatTerm<F>>>>,
465 ) -> Vec<FlatTerm<F>> {
466 if let Some(ref cached) = cache[id.0 as usize] {
467 return cached.clone();
468 }
469
470 let result = match arena.get(id) {
471 ConstraintExpr::Cell(cell) => {
472 vec![(F::ONE, vec![*cell])]
473 }
474 ConstraintExpr::Const(k) => {
475 vec![(*k, vec![])]
476 }
477 ConstraintExpr::Add(a, b) => {
478 let mut terms = expand(arena, *a, cache);
479 terms.extend(expand(arena, *b, cache));
480
481 terms
482 }
483 ConstraintExpr::Mul(a, b) => {
484 let left = expand(arena, *a, cache);
485 let right = expand(arena, *b, cache);
486
487 let mut terms = Vec::with_capacity(left.len() * right.len());
488 for (lc, lp) in &left {
489 for (rc, rp) in &right {
490 let coeff = *lc * *rc;
491 let mut cells = lp.clone();
492
493 cells.extend_from_slice(rp);
494 terms.push((coeff, cells));
495 }
496 }
497
498 terms
499 }
500 ConstraintExpr::Scale(k, a) => {
501 let inner = expand(arena, *a, cache);
502 inner
503 .into_iter()
504 .map(|(c, cells)| (*k * c, cells))
505 .collect()
506 }
507 ConstraintExpr::Sum(children) => {
508 let mut terms = Vec::new();
509 for child in children {
510 terms.extend(expand(arena, *child, cache));
511 }
512
513 terms
514 }
515 };
516
517 cache[id.0 as usize] = Some(result.clone());
518
519 result
520 }
521
522 let n = self.arena.len();
523 let mut cache: Vec<Option<Vec<FlatTerm<F>>>> = vec![None; n];
524
525 self.roots
526 .iter()
527 .map(|root| {
528 let flat_terms = expand(&self.arena, *root, &mut cache);
529 Constraint::new(
530 flat_terms
531 .into_iter()
532 .map(|(coeff, cells)| ConstraintTerm::new(coeff, cells))
533 .collect(),
534 )
535 })
536 .collect()
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543 use crate::constraint::ConstraintExpr;
544 use crate::constraint::builder::ConstraintSystem;
545 use crate::{Air, Program};
546 use hekate_core::trace::ColumnType;
547 use hekate_math::{Block128, Flat};
548
549 type F = Block128;
550
551 #[derive(Clone)]
552 struct TestFibProgram;
553
554 impl Air<F> for TestFibProgram {
555 fn num_columns(&self) -> usize {
556 3
557 }
558
559 fn column_layout(&self) -> &[ColumnType] {
560 &[ColumnType::B32, ColumnType::B32, ColumnType::Bit]
561 }
562
563 fn constraint_ast(&self) -> ConstraintAst<F> {
564 let cs = ConstraintSystem::<F>::new();
565
566 let [a, b, q] = [cs.col(0), cs.col(1), cs.col(2)];
567 let [na, nb] = [cs.next(0), cs.next(1)];
568
569 cs.constrain(q * (na + b));
570 cs.constrain(q * (nb + a + b));
571
572 cs.build()
573 }
574 }
575
576 impl Program<F> for TestFibProgram {}
577
578 #[test]
579 fn default_constraint_ast_produces_correct_roots() {
580 let program = TestFibProgram;
581 let ast = program.constraint_ast();
582
583 assert_eq!(ast.roots.len(), 2);
585
586 assert!(!ast.arena.is_empty());
591
592 for &root in &ast.roots {
594 let node = ast.arena.get(root);
595 match node {
596 ConstraintExpr::Sum(children) => {
597 assert!(!children.is_empty());
598 }
599 ConstraintExpr::Mul(_, _) => {
600 }
602 _ => panic!("Root should be Sum or Mul, got {:?}", node),
603 }
604 }
605 }
606
607 #[test]
608 fn cell_dedup_works() {
609 let mut arena = ConstraintArena::<F>::new();
610
611 let cell_a = ProgramCell::current(0);
612 let cell_b = ProgramCell::current(0);
613 let cell_c = ProgramCell::next(0);
614
615 let id_a = arena.cell(cell_a);
616 let id_b = arena.cell(cell_b);
617 let id_c = arena.cell(cell_c);
618
619 assert_eq!(id_a, id_b);
621 assert_ne!(id_a, id_c);
623 assert_eq!(arena.len(), 2);
625 }
626
627 #[test]
628 fn dag_sharing_reduces_node_count() {
629 let mut arena = ConstraintArena::<F>::new();
630
631 let a = arena.cell(ProgramCell::current(0));
633 let b = arena.cell(ProgramCell::current(1));
634 let c = arena.cell(ProgramCell::current(2));
635 let theta = arena.sum(vec![a, b, c]);
636
637 let d = arena.cell(ProgramCell::current(3));
639 let expr1 = arena.mul(theta, d);
640 let expr2 = arena.mul(theta, a); let dag_node_count = arena.len();
643 assert_eq!(dag_node_count, 7);
645
646 assert!(dag_node_count < 10);
652
653 match arena.get(expr1) {
655 ConstraintExpr::Mul(lhs, _) => assert_eq!(*lhs, theta),
656 _ => panic!("Expected Mul"),
657 }
658
659 match arena.get(expr2) {
660 ConstraintExpr::Mul(lhs, rhs) => {
661 assert_eq!(*lhs, theta);
662 assert_eq!(*rhs, a);
663 }
664 _ => panic!("Expected Mul"),
665 }
666 }
667
668 #[test]
669 fn default_constraint_ast_node_count_matches_flat() {
670 let program = TestFibProgram;
671 let flat = program.constraints();
672 let ast = program.constraint_ast();
673
674 let mut flat_cell_count = 0;
676 for c in &flat {
677 for t in &c.terms {
678 flat_cell_count += t.poly_ind.len();
679 }
680 }
681
682 let ast_cell_count = ast
684 .arena
685 .nodes
686 .iter()
687 .filter(|n| matches!(n, ConstraintExpr::Cell(_)))
688 .count();
689
690 assert!(ast_cell_count <= flat_cell_count);
691
692 assert_eq!(ast_cell_count, 5);
697 }
698
699 #[test]
700 fn empty_constraint_produces_empty_ast() {
701 #[derive(Clone)]
702 struct EmptyProgram;
703
704 impl Air<F> for EmptyProgram {
705 fn num_columns(&self) -> usize {
706 0
707 }
708
709 fn column_layout(&self) -> &[ColumnType] {
710 &[]
711 }
712
713 fn constraint_ast(&self) -> ConstraintAst<F> {
714 ConstraintSystem::<F>::new().build()
715 }
716 }
717
718 impl Program<F> for EmptyProgram {}
719
720 let ast = EmptyProgram.constraint_ast();
721 assert!(ast.roots.is_empty());
722 assert!(ast.arena.is_empty());
723 }
724
725 #[test]
726 fn single_term_constraint_no_sum_wrapper() {
727 #[derive(Clone)]
728 struct SingleTermProgram;
729
730 impl Air<F> for SingleTermProgram {
731 fn num_columns(&self) -> usize {
732 2
733 }
734
735 fn column_layout(&self) -> &[ColumnType] {
736 &[ColumnType::B128, ColumnType::B128]
737 }
738
739 fn constraint_ast(&self) -> ConstraintAst<F> {
740 let cs = ConstraintSystem::<F>::new();
741 cs.constrain(cs.col(0) * cs.col(1));
742
743 cs.build()
744 }
745 }
746
747 impl Program<F> for SingleTermProgram {}
748
749 let ast = SingleTermProgram.constraint_ast();
750 assert_eq!(ast.roots.len(), 1);
751
752 match ast.arena.get(ast.roots[0]) {
754 ConstraintExpr::Mul(_, _) => {} other => panic!("Expected Mul for single-term, got {:?}", other),
756 }
757 }
758
759 #[test]
764 fn max_degree_fibonacci() {
765 let program = TestFibProgram;
766 let ast = program.constraint_ast();
767
768 let flat = program.constraints();
773 let flat_max = flat
774 .iter()
775 .flat_map(|c| c.terms.iter())
776 .map(|t| t.poly_ind.len())
777 .max()
778 .unwrap_or(0);
779
780 assert_eq!(ast.max_degree(), flat_max);
781 }
782
783 #[test]
784 fn max_degree_empty() {
785 let ast = ConstraintAst::<F> {
786 arena: ConstraintArena::new(),
787 roots: Vec::new(),
788 labels: Vec::new(),
789 };
790 assert_eq!(ast.max_degree(), 0);
791 }
792
793 #[test]
794 fn max_degree_builder_mul_chain() {
795 use crate::constraint::builder::ConstraintSystem;
796
797 let cs = ConstraintSystem::<F>::new();
798 let a = cs.col(0);
799 let b = cs.col(1);
800 let c = cs.col(2);
801
802 cs.constrain(a * b * c);
804 cs.constrain(a + b);
806
807 let ast = cs.build();
808 assert_eq!(ast.max_degree(), 3);
809 }
810
811 #[test]
812 fn to_constraints_roundtrip_fibonacci() {
813 let program = TestFibProgram;
814 let ast = program.constraint_ast();
815 let flat_from_ast = ast.to_constraints();
816 let flat_direct = program.constraints();
817
818 assert_eq!(flat_from_ast.len(), flat_direct.len());
820
821 for (a, d) in flat_from_ast.iter().zip(flat_direct.iter()) {
823 assert_eq!(a.terms.len(), d.terms.len());
824 }
825 }
826
827 #[test]
828 fn to_constraints_from_builder() {
829 use crate::constraint::builder::ConstraintSystem;
830
831 let cs = ConstraintSystem::<F>::new();
832 let a = cs.col(0);
833 let b = cs.col(1);
834
835 cs.constrain(a + b);
837
838 let ast = cs.build();
839 let flat = ast.to_constraints();
840
841 assert_eq!(flat.len(), 1);
842 assert_eq!(flat[0].terms.len(), 2);
843
844 for term in &flat[0].terms {
846 assert_eq!(term.coeff, F::ONE);
847 assert_eq!(term.poly_ind.len(), 1);
848 }
849 }
850
851 #[test]
852 fn evaluate_simple_constraint() {
853 use crate::constraint::builder::ConstraintSystem;
854 use hekate_math::Flat;
855
856 let cs = ConstraintSystem::<F>::new();
857 let a = cs.col(0);
858 let b = cs.col(1);
859
860 cs.constrain(a + b);
862
863 let ast = cs.build();
864
865 let current = vec![
867 Flat::from_raw(F::from(3u128)),
868 Flat::from_raw(F::from(3u128)),
869 ];
870 let next = vec![Flat::from_raw(F::ZERO); 2];
871
872 let evals = ast.evaluate(¤t, &next);
873 assert_eq!(evals.len(), 1);
874 assert_eq!(evals[0], Flat::from_raw(F::ZERO)); let current2 = vec![
878 Flat::from_raw(F::from(3u128)),
879 Flat::from_raw(F::from(5u128)),
880 ];
881 let evals2 = ast.evaluate(¤t2, &next);
882 assert_ne!(evals2[0], Flat::from_raw(F::ZERO));
883 }
884
885 #[test]
886 fn evaluate_into_matches_evaluate() {
887 let cs = ConstraintSystem::<F>::new();
888
889 let a = cs.col(0);
890 let b = cs.col(1);
891 let na = cs.next(0);
892
893 cs.constrain(a + b);
894 cs.constrain(a * b);
895 cs.constrain(na + a);
896
897 let ast = cs.build();
898 let zero = Flat::from_raw(F::ZERO);
899
900 let current = vec![
901 Flat::from_raw(F::from(3u128)),
902 Flat::from_raw(F::from(5u128)),
903 ];
904 let next = vec![Flat::from_raw(F::from(7u128)), zero];
905
906 let expected = ast.evaluate(¤t, &next);
907
908 let consts = ast.precompute_hardware_consts();
909
910 let mut buf = Vec::new();
911 ast.evaluate_into(&consts, ¤t, &next, &mut buf);
912
913 for (i, root) in ast.roots.iter().enumerate() {
914 assert_eq!(buf[root.0 as usize], expected[i]);
915 }
916 }
917}