1use crate::chiplet::ChipletDef;
13use crate::constraint::builder::ConstraintSystem;
14use crate::constraint::{BoundaryConstraint, BoundaryTarget, ConstraintAst};
15use crate::expander::VirtualExpander;
16use crate::permutation::{
17 PermutationCheckSpec, Service, Source, validate_bus_set, validate_fixed_selectors,
18};
19use crate::{Air, FixedColumn, FixedShape, InlineKernelHint, Program, validate_fixed_columns};
20use alloc::string::String;
21use alloc::vec::Vec;
22use hekate_core::errors;
23use hekate_core::trace::ColumnType;
24use hekate_math::{HardwareField, TowerField};
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct Col(usize);
29
30impl Col {
31 #[inline(always)]
32 pub fn index(self) -> usize {
33 self.0
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct ColRange {
40 start: usize,
41 count: usize,
42}
43
44impl ColRange {
45 pub fn at(self, i: usize) -> Col {
46 assert!(i < self.count, "ColRange::at out of range");
47 Col(self.start + i)
48 }
49
50 #[inline(always)]
51 pub fn start(self) -> usize {
52 self.start
53 }
54
55 #[inline(always)]
56 pub fn len(self) -> usize {
57 self.count
58 }
59
60 #[inline(always)]
61 pub fn is_empty(self) -> bool {
62 self.count == 0
63 }
64
65 pub fn iter(self) -> impl Iterator<Item = Col> {
66 (self.start..self.start + self.count).map(Col)
67 }
68}
69
70#[derive(Clone, Copy, Debug)]
72pub struct Packed {
73 phys_start: usize,
74 count: usize,
75 bits_per: usize,
76 bits: ColRange,
77}
78
79impl Packed {
80 pub fn bits(&self, i: usize) -> ColRange {
85 assert!(i < self.count, "Packed::bits out of range");
86 ColRange {
87 start: self.bits.start + i * self.bits_per,
88 count: self.bits_per,
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug)]
95pub struct Mounted {
96 offset: usize,
97 num_columns: usize,
98}
99
100impl Mounted {
101 pub fn col(&self, local: usize) -> Col {
104 assert!(local < self.num_columns, "Mounted::col out of range");
105 Col(self.offset + local)
106 }
107
108 #[inline(always)]
109 pub fn offset(&self) -> usize {
110 self.offset
111 }
112
113 #[inline(always)]
114 pub fn num_columns(&self) -> usize {
115 self.num_columns
116 }
117}
118
119pub struct Circuit<F: TowerField> {
122 name: String,
123 num_rows: usize,
124 cs: ConstraintSystem<F>,
125 physical: Vec<ColumnType>,
126 expander: VirtualExpander,
127 pending_run: Option<(ColumnType, usize)>,
128 num_virtual: usize,
129 fixed: Vec<FixedColumn<F>>,
130 boundaries: Vec<BoundaryConstraint<F>>,
131 buses: Vec<(String, PermutationCheckSpec)>,
132 num_public_inputs: usize,
133 inline_chiplets: Vec<ChipletDef<F>>,
134 inline_hints: Vec<InlineKernelHint>,
135 chiplet_defs: Vec<ChipletDef<F>>,
136}
137
138impl<F: TowerField + HardwareField> Circuit<F> {
139 pub fn new(name: &str, num_rows: usize) -> errors::Result<Self> {
142 if !num_rows.is_power_of_two() {
143 return Err(errors::Error::Protocol {
144 protocol: "circuit",
145 message: "num_rows must be a power of two",
146 });
147 }
148
149 Ok(Self {
150 name: String::from(name),
151 num_rows,
152 cs: ConstraintSystem::new(),
153 physical: Vec::new(),
154 expander: VirtualExpander::new(),
155 pending_run: None,
156 num_virtual: 0,
157 fixed: Vec::new(),
158 boundaries: Vec::new(),
159 buses: Vec::new(),
160 num_public_inputs: 0,
161 inline_chiplets: Vec::new(),
162 inline_hints: Vec::new(),
163 chiplet_defs: Vec::new(),
164 })
165 }
166
167 #[inline(always)]
168 pub fn num_rows(&self) -> usize {
169 self.num_rows
170 }
171
172 pub fn cs(&self) -> &ConstraintSystem<F> {
175 &self.cs
176 }
177
178 pub fn column(&mut self, ty: ColumnType) -> Col {
179 self.columns(1, ty).at(0)
180 }
181
182 pub fn schema(&mut self, layout: &[ColumnType]) -> ColRange {
185 let start = self.num_virtual;
186
187 for &ty in layout {
188 self.column(ty);
189 }
190
191 ColRange {
192 start,
193 count: layout.len(),
194 }
195 }
196
197 pub fn columns(&mut self, count: usize, ty: ColumnType) -> ColRange {
198 let start = self.num_virtual;
199
200 self.physical.extend(core::iter::repeat_n(ty, count));
201 self.num_virtual += count;
202
203 match self.pending_run.take() {
204 Some((run_ty, run_count)) if run_ty == ty => {
205 self.pending_run = Some((ty, run_count + count));
206 }
207 prior => {
208 self.flush_run(prior);
209
210 self.pending_run = Some((ty, count));
211 }
212 }
213
214 ColRange { start, count }
215 }
216
217 pub fn expand_bits(&mut self, count: usize, storage: ColumnType) -> Packed {
220 self.flush_pending();
221
222 let phys_start = self.physical.len();
223 let bits_per = storage.byte_size() * 8;
224 let bits = ColRange {
225 start: self.num_virtual,
226 count: count * bits_per,
227 };
228
229 self.physical.extend(core::iter::repeat_n(storage, count));
230 self.num_virtual += bits.count;
231
232 let expander = core::mem::take(&mut self.expander);
233
234 self.expander = expander.expand_bits(count, storage);
235
236 Packed {
237 phys_start,
238 count,
239 bits_per,
240 bits,
241 }
242 }
243
244 pub fn reuse_pass_through(&mut self, packed: &Packed) -> ColRange {
247 self.flush_pending();
248
249 let view = ColRange {
250 start: self.num_virtual,
251 count: packed.count,
252 };
253
254 self.num_virtual += packed.count;
255
256 let expander = core::mem::take(&mut self.expander);
257
258 self.expander = expander.reuse_pass_through(packed.phys_start, packed.count);
259
260 view
261 }
262
263 pub fn fix(&mut self, col: Col, shape: FixedShape<F>) {
266 self.fixed.push(FixedColumn {
267 col_idx: col.index(),
268 shape,
269 });
270 }
271
272 pub fn boundary(&mut self, col: Col, row: usize, value: F) {
275 self.boundaries.push(BoundaryConstraint {
276 col_idx: col.index(),
277 row_idx: row,
278 target: BoundaryTarget::Constant(value),
279 });
280 }
281
282 pub fn call(&mut self, service: &Service, values: &[Col], selector: Col) -> errors::Result<()> {
284 let cols: Vec<usize> = values.iter().map(|c| c.index()).collect();
285 let spec = service.request(&cols, selector.index())?;
286
287 self.buses.push((String::from(service.bus_id), spec));
288
289 Ok(())
290 }
291
292 pub fn bus(&mut self, bus_id: &str, spec: PermutationCheckSpec) {
295 self.buses.push((String::from(bus_id), spec));
296 }
297
298 pub fn mount(&mut self, def: ChipletDef<F>) -> Mounted {
301 self.mount_inner(def, true)
302 }
303
304 pub fn mount_unlinked(&mut self, def: ChipletDef<F>) -> Mounted {
307 self.mount_inner(def, false)
308 }
309
310 pub fn attach(&mut self, def: ChipletDef<F>) {
313 self.chiplet_defs.push(def);
314 }
315
316 pub fn publish(&mut self, col: Col, row: usize) -> usize {
320 let slot = self.next_slot();
321
322 self.boundaries.push(BoundaryConstraint::with_public_input(
323 col.index(),
324 row,
325 slot,
326 ));
327
328 slot
329 }
330
331 pub fn compile(mut self) -> errors::Result<CircuitProgram<F>> {
334 self.flush_pending();
335
336 let expander = self.expander.build()?;
337 let width = self.num_virtual;
338 let num_vars = self.num_rows.trailing_zeros() as usize;
339
340 if expander.num_virtual_columns() != width {
341 return Err(errors::Error::Protocol {
342 protocol: "circuit",
343 message: "expander virtual width diverged from declared columns",
344 });
345 }
346
347 let one_to_one = expander.num_virtual_columns() == expander.num_physical_columns();
348 let virtual_layout = expander.virtual_layout().to_vec();
349
350 let ast = self.cs.build();
351
352 for (bus_id, spec) in &self.buses {
353 spec.validate_clock_stitching(bus_id)?;
354
355 for (source, _) in &spec.sources {
356 validate_source_range(source, width)?;
357 }
358 }
359
360 validate_fixed_selectors(&self.buses, &self.fixed)?;
361
362 for bc in &self.boundaries {
363 if bc.col_idx >= width || bc.row_idx >= self.num_rows {
364 return Err(errors::Error::Protocol {
365 protocol: "circuit",
366 message: "boundary constraint outside the trace",
367 });
368 }
369 }
370
371 validate_fixed_columns(&self.fixed, &virtual_layout, Some(num_vars))?;
372
373 for bc in &self.boundaries {
374 if let BoundaryTarget::PublicInput(slot) = bc.target
375 && slot >= self.num_public_inputs
376 {
377 return Err(errors::Error::Protocol {
378 protocol: "circuit",
379 message: "boundary public input slot out of range",
380 });
381 }
382 }
383
384 let chiplet_specs: Vec<Vec<(String, PermutationCheckSpec)>> = self
385 .chiplet_defs
386 .iter()
387 .map(|cd| cd.permutation_checks())
388 .collect();
389
390 let endpoints = self
391 .buses
392 .iter()
393 .chain(chiplet_specs.iter().flatten())
394 .map(|(id, s)| (id.as_str(), s));
395
396 validate_bus_set(endpoints)?;
397
398 let table = ChipletDef::from_parts(
399 self.name,
400 width,
401 ast,
402 self.physical,
403 virtual_layout,
404 self.boundaries,
405 self.fixed,
406 (!one_to_one).then_some(expander),
407 self.buses,
408 );
409
410 Ok(CircuitProgram {
411 table,
412 num_public_inputs: self.num_public_inputs,
413 inline_chiplets: self.inline_chiplets,
414 inline_kernels: self.inline_hints,
415 chiplet_defs: self.chiplet_defs,
416 })
417 }
418
419 fn mount_inner(&mut self, def: ChipletDef<F>, link: bool) -> Mounted {
420 self.flush_pending();
421
422 let chiplet_idx = self.inline_chiplets.len();
423 let offset = self.num_virtual;
424 let num_columns = Air::<F>::num_columns(&def);
425
426 let mut ast = def.constraint_ast();
427 ast.arena.shift_cells(offset);
428
429 self.inline_hints.push(InlineKernelHint {
430 chiplet_idx,
431 root_offset: self.cs.num_roots(),
432 column_offset: offset,
433 });
434
435 self.cs.merge_ast(ast);
436
437 for mut fc in Air::<F>::fixed_columns(&def) {
438 fc.col_idx += offset;
439 self.fixed.push(fc);
440 }
441
442 for mut bc in def.boundary_constraints() {
443 bc.col_idx += offset;
444 self.boundaries.push(bc);
445 }
446
447 match def.virtual_expander() {
448 Some(e) => {
449 let expander = core::mem::take(&mut self.expander);
450
451 self.expander = expander.append(e);
452 self.physical.extend_from_slice(def.column_layout());
453 self.num_virtual += num_columns;
454 }
455 None => {
456 self.schema(def.column_layout());
457 }
458 }
459
460 if link {
461 for (bus_id, mut spec) in def.permutation_checks() {
462 spec.shift_column_indices(offset);
463 self.buses.push((bus_id, spec));
464 }
465 }
466
467 self.inline_chiplets.push(def);
468
469 Mounted {
470 offset,
471 num_columns,
472 }
473 }
474
475 fn next_slot(&mut self) -> usize {
476 let slot = self.num_public_inputs;
477 self.num_public_inputs += 1;
478
479 slot
480 }
481
482 fn flush_pending(&mut self) {
483 let prior = self.pending_run.take();
484 self.flush_run(prior);
485 }
486
487 fn flush_run(&mut self, run: Option<(ColumnType, usize)>) {
488 let Some((ty, count)) = run else {
489 return;
490 };
491
492 let expander = core::mem::take(&mut self.expander);
493 self.expander = match ty {
494 ColumnType::Bit => expander.control_bits(count),
495 _ => expander.pass_through(count, ty),
496 };
497 }
498}
499
500#[derive(Clone)]
502pub struct CircuitProgram<F: TowerField> {
503 table: ChipletDef<F>,
504 num_public_inputs: usize,
505 inline_chiplets: Vec<ChipletDef<F>>,
506 inline_kernels: Vec<InlineKernelHint>,
507 chiplet_defs: Vec<ChipletDef<F>>,
508}
509
510impl<F: TowerField> Air<F> for CircuitProgram<F> {
511 fn name(&self) -> String {
512 self.table.name()
513 }
514
515 fn num_columns(&self) -> usize {
516 self.table.num_columns()
517 }
518
519 fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
520 self.table.boundary_constraints()
521 }
522
523 fn column_layout(&self) -> &[ColumnType] {
524 self.table.column_layout()
525 }
526
527 fn virtual_column_layout(&self) -> &[ColumnType] {
528 self.table.virtual_column_layout()
529 }
530
531 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
532 self.table.permutation_checks()
533 }
534
535 fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
536 self.table.fixed_columns()
537 }
538
539 fn virtual_expander(&self) -> Option<&VirtualExpander> {
540 self.table.virtual_expander()
541 }
542
543 fn constraint_ast(&self) -> ConstraintAst<F> {
544 self.table.constraint_ast()
545 }
546
547 fn inline_chiplets(&self) -> errors::Result<Vec<ChipletDef<F>>> {
548 Ok(self.inline_chiplets.clone())
549 }
550
551 fn inline_chiplet_kernels(&self) -> Vec<InlineKernelHint> {
552 self.inline_kernels.clone()
553 }
554}
555
556impl<F: TowerField> Program<F> for CircuitProgram<F> {
557 fn num_public_inputs(&self) -> usize {
558 self.num_public_inputs
559 }
560
561 fn chiplet_defs(&self) -> errors::Result<Vec<ChipletDef<F>>> {
562 Ok(self.chiplet_defs.clone())
563 }
564}
565
566fn validate_source_range(source: &Source, width: usize) -> errors::Result<()> {
567 let out_of_range = match source {
568 Source::Column(idx) | Source::PhaseColumn(idx) => *idx >= width,
569 Source::Columns(indices) => indices.iter().any(|&idx| idx >= width),
570 Source::RowIndexLeBytes(_) | Source::RowIndexByte(_) | Source::Const(_) => false,
571 };
572
573 if out_of_range {
574 return Err(errors::Error::Protocol {
575 protocol: "circuit",
576 message: "bus source column out of range",
577 });
578 }
579
580 Ok(())
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use crate::permutation::{REQUEST_IDX_LABEL, Source};
587 use alloc::string::ToString;
588 use alloc::vec;
589 use hekate_math::Block128;
590
591 type F = Block128;
592
593 #[derive(Clone)]
594 struct MiniChiplet;
595
596 impl Air<F> for MiniChiplet {
597 fn name(&self) -> String {
598 "MiniChiplet".to_string()
599 }
600
601 fn num_columns(&self) -> usize {
602 2
603 }
604
605 fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
606 vec![BoundaryConstraint::with_constant(0, 0, F::ZERO)]
607 }
608
609 fn column_layout(&self) -> &[ColumnType] {
610 &[ColumnType::B32, ColumnType::Bit]
611 }
612
613 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
614 vec![(
615 "mini_bus".to_string(),
616 PermutationCheckSpec::new(
617 vec![
618 (Source::Column(0), b"k_v"),
619 (Source::RowIndexLeBytes(4), REQUEST_IDX_LABEL),
620 ],
621 Some(1),
622 ),
623 )]
624 }
625
626 fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
627 vec![FixedColumn::prefix(1, 1)]
628 }
629
630 fn constraint_ast(&self) -> ConstraintAst<F> {
631 let cs = ConstraintSystem::<F>::new();
632 cs.assert_boolean(cs.col(1));
633
634 cs.build()
635 }
636 }
637
638 fn mini_def() -> ChipletDef<F> {
639 ChipletDef::from_air(&MiniChiplet).unwrap()
640 }
641
642 fn clocked_spec(key_col: usize, selector: usize) -> PermutationCheckSpec {
643 PermutationCheckSpec::new(
644 vec![
645 (Source::Column(key_col), b"k_v"),
646 (Source::RowIndexLeBytes(4), REQUEST_IDX_LABEL),
647 ],
648 Some(selector),
649 )
650 }
651
652 #[test]
653 fn mount_matches_manual_shift() {
654 let mut cx = Circuit::<F>::new("mini_host", 16).unwrap();
655 let host_key = cx.column(ColumnType::B32);
656
657 let cs = cx.cs();
658 cs.constrain(cs.col(host_key.index()) * cs.col(host_key.index()));
659
660 let mounted = cx.mount(mini_def());
661
662 assert_eq!(mounted.offset(), 1);
663
664 let program = cx.compile().unwrap();
665
666 assert_eq!(
667 program.column_layout(),
668 &[ColumnType::B32, ColumnType::B32, ColumnType::Bit]
669 );
670
671 let hints = program.inline_chiplet_kernels();
672
673 assert_eq!(hints.len(), 1);
674 assert_eq!(hints[0].chiplet_idx, 0);
675 assert_eq!(hints[0].root_offset, 1);
676 assert_eq!(hints[0].column_offset, 1);
677
678 let mut manual = {
679 let cs = ConstraintSystem::<F>::new();
680 cs.constrain(cs.col(0) * cs.col(0));
681
682 cs.build()
683 };
684
685 let mut chiplet_ast = Air::<F>::constraint_ast(&MiniChiplet);
686 chiplet_ast.arena.shift_cells(1);
687
688 manual.merge(chiplet_ast);
689
690 assert_eq!(
691 program.constraint_ast().to_constraints(),
692 manual.to_constraints()
693 );
694
695 let specs = program.permutation_checks();
696
697 assert_eq!(specs.len(), 1);
698 assert_eq!(specs[0].1.selector, Some(2));
699 assert_eq!(specs[0].1.sources[0].0, Source::Column(1));
700
701 let pins = program.fixed_columns();
702
703 assert_eq!(pins.len(), 1);
704 assert_eq!(pins[0].col_idx, 2);
705
706 let boundaries = program.boundary_constraints();
707
708 assert_eq!(boundaries.len(), 1);
709 assert_eq!(boundaries[0].col_idx, 1);
710 }
711
712 #[test]
713 fn mount_unlinked_keeps_columns_and_drops_endpoint() {
714 let mut cx = Circuit::<F>::new("isolated_host", 16).unwrap();
715 let mounted = cx.mount_unlinked(mini_def());
716
717 let program = cx.compile().unwrap();
718
719 assert_eq!(mounted.offset(), 0);
720 assert_eq!(program.num_columns(), 2);
721 assert!(program.permutation_checks().is_empty());
722 assert_eq!(program.fixed_columns().len(), 1);
723 }
724
725 #[test]
726 fn plain_columns_elide_expander() {
727 let mut cx = Circuit::<F>::new("plain", 16).unwrap();
728 cx.columns(3, ColumnType::B64);
729 cx.column(ColumnType::Bit);
730
731 let program = cx.compile().unwrap();
732
733 assert!(program.virtual_expander().is_none());
734 assert_eq!(program.num_columns(), 4);
735 assert_eq!(program.column_layout(), program.virtual_column_layout());
736 }
737
738 #[test]
739 fn publish_assigns_sequential_slots() {
740 let mut cx = Circuit::<F>::new("slots", 16).unwrap();
741 let val = cx.column(ColumnType::B32);
742
743 let s0 = cx.publish(val, 3);
744 let s1 = cx.publish(val, 5);
745
746 assert_eq!((s0, s1), (0, 1));
747
748 let program = cx.compile().unwrap();
749
750 assert_eq!(program.num_public_inputs(), 2);
751
752 let boundaries = program.boundary_constraints();
753 assert_eq!(boundaries.len(), 2);
754 assert_eq!(boundaries[0].target, BoundaryTarget::PublicInput(0));
755 assert_eq!(boundaries[1].target, BoundaryTarget::PublicInput(1));
756 }
757
758 #[test]
759 fn compile_rejects_witness_bus_selector() {
760 let mut cx = Circuit::<F>::new("discipline", 16).unwrap();
761 let key = cx.column(ColumnType::B32);
762 let sel = cx.column(ColumnType::Bit);
763
764 cx.bus("lone_bus", clocked_spec(key.index(), sel.index()));
765
766 assert!(cx.compile().is_err());
767 }
768
769 #[test]
770 fn compile_keeps_pinned_selector_unrooted() {
771 let mut cx = Circuit::<F>::new("pinned", 16).unwrap();
772 let key = cx.column(ColumnType::B32);
773 let sel = cx.column(ColumnType::Bit);
774
775 cx.fix(sel, FixedShape::Sparse(vec![(0, F::ONE)]));
776 cx.bus("lone_bus", clocked_spec(key.index(), sel.index()));
777
778 assert!(cx.compile().unwrap().constraint_ast().roots.is_empty());
779 }
780
781 #[test]
782 fn compile_rejects_out_of_range_bus_source() {
783 let mut cx = Circuit::<F>::new("oor", 16).unwrap();
784 let sel = cx.column(ColumnType::Bit);
785
786 cx.bus("bad_bus", clocked_spec(99, sel.index()));
787
788 assert!(cx.compile().is_err());
789 }
790
791 #[test]
792 fn mounted_expander_is_kept_and_offset() {
793 let expander_def = {
794 #[derive(Clone)]
795 struct Packed {
796 expander: VirtualExpander,
797 }
798
799 impl Air<F> for Packed {
800 fn num_columns(&self) -> usize {
801 self.expander.num_virtual_columns()
802 }
803
804 fn column_layout(&self) -> &[ColumnType] {
805 &[ColumnType::B32, ColumnType::Bit]
806 }
807
808 fn virtual_expander(&self) -> Option<&VirtualExpander> {
809 Some(&self.expander)
810 }
811
812 fn constraint_ast(&self) -> ConstraintAst<F> {
813 ConstraintSystem::<F>::new().build()
814 }
815 }
816
817 ChipletDef::from_air(&Packed {
818 expander: VirtualExpander::new()
819 .expand_bits(1, ColumnType::B32)
820 .control_bits(1)
821 .build()
822 .unwrap(),
823 })
824 .unwrap()
825 };
826
827 let mut cx = Circuit::<F>::new("packed_host", 16).unwrap();
828
829 let host = cx.column(ColumnType::B64);
830 let mounted = cx.mount(expander_def);
831
832 assert_eq!(host.index(), 0);
833 assert_eq!(mounted.offset(), 1);
834 assert_eq!(mounted.num_columns(), 33);
835
836 let program = cx.compile().unwrap();
837
838 assert!(program.virtual_expander().is_some());
839 assert_eq!(program.num_columns(), 34);
840 assert_eq!(program.virtual_column_layout()[0], ColumnType::B64);
841 assert_eq!(program.virtual_column_layout()[1], ColumnType::Bit);
842 assert_eq!(program.virtual_column_layout()[33], ColumnType::Bit);
843 }
844}