1use alloc::string::String;
13use alloc::vec::Vec;
14use core::ops::Range;
15use hekate_core::errors;
16use hekate_core::trace::ColumnType;
17use hekate_math::{HardwareField, TowerField};
18
19use crate::chiplet::ChipletDef;
20use crate::constraint::builder::ConstraintSystem;
21use crate::constraint::{BoundaryConstraint, BoundaryTarget, ConstraintAst};
22use crate::expander::VirtualExpander;
23use crate::permutation::{
24 PermutationCheckSpec, RankTable, Service, Source, TableHeight, validate_bus_set,
25 validate_fixed_selectors, validate_ordered_emits,
26};
27use crate::{Air, FixedColumn, FixedShape, InlineKernelHint, Program, validate_fixed_columns};
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct Col(usize);
32
33impl Col {
34 #[inline(always)]
35 pub fn index(self) -> usize {
36 self.0
37 }
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct ColRange {
43 start: usize,
44 count: usize,
45}
46
47impl ColRange {
48 pub fn at(self, i: usize) -> Col {
49 assert!(i < self.count, "ColRange::at out of range");
50 Col(self.start + i)
51 }
52
53 #[inline(always)]
54 pub fn start(self) -> usize {
55 self.start
56 }
57
58 #[inline(always)]
59 pub fn len(self) -> usize {
60 self.count
61 }
62
63 #[inline(always)]
64 pub fn is_empty(self) -> bool {
65 self.count == 0
66 }
67
68 pub fn iter(self) -> impl Iterator<Item = Col> {
69 (self.start..self.start + self.count).map(Col)
70 }
71}
72
73#[derive(Clone, Copy, Debug)]
75pub struct Packed {
76 phys_start: usize,
77 count: usize,
78 bits_per: usize,
79 bits: ColRange,
80}
81
82impl Packed {
83 pub fn bits(&self, i: usize) -> ColRange {
88 assert!(i < self.count, "Packed::bits out of range");
89 ColRange {
90 start: self.bits.start + i * self.bits_per,
91 count: self.bits_per,
92 }
93 }
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub struct PhysRange {
99 start: usize,
100 count: usize,
101}
102
103impl PhysRange {
104 pub fn slice(self, start: usize, count: usize) -> PhysRange {
106 assert!(
107 start
108 .checked_add(count)
109 .is_some_and(|end| end <= self.count),
110 "PhysRange::slice out of range"
111 );
112
113 PhysRange {
114 start: self.start + start,
115 count,
116 }
117 }
118
119 #[inline(always)]
120 pub fn len(self) -> usize {
121 self.count
122 }
123
124 #[inline(always)]
125 pub fn is_empty(self) -> bool {
126 self.count == 0
127 }
128}
129
130impl From<&Packed> for PhysRange {
131 fn from(packed: &Packed) -> Self {
132 PhysRange {
133 start: packed.phys_start,
134 count: packed.count,
135 }
136 }
137}
138
139#[derive(Clone, Copy, Debug)]
141pub struct Mounted {
142 offset: usize,
143 num_columns: usize,
144 physical: PhysRange,
145}
146
147impl Mounted {
148 pub fn col(&self, local: usize) -> Col {
151 assert!(local < self.num_columns, "Mounted::col out of range");
152 Col(self.offset + local)
153 }
154
155 #[inline(always)]
156 pub fn offset(&self) -> usize {
157 self.offset
158 }
159
160 #[inline(always)]
161 pub fn num_columns(&self) -> usize {
162 self.num_columns
163 }
164
165 #[inline(always)]
168 pub fn physical(&self) -> PhysRange {
169 self.physical
170 }
171}
172
173pub struct Circuit<F: TowerField> {
176 name: String,
177 num_rows: usize,
178 cs: ConstraintSystem<F>,
179 physical: Vec<ColumnType>,
180 expander: VirtualExpander,
181 pending_run: Option<(ColumnType, usize)>,
182 num_virtual: usize,
183 fixed: Vec<FixedColumn<F>>,
184 boundaries: Vec<BoundaryConstraint<F>>,
185 buses: Vec<(String, PermutationCheckSpec)>,
186 num_public_inputs: usize,
187 inline_chiplets: Vec<ChipletDef<F>>,
188 inline_hints: Vec<InlineKernelHint>,
189 chiplet_defs: Vec<ChipletDef<F>>,
190 namespaces: Vec<(String, Range<usize>)>,
191}
192
193impl<F: TowerField + HardwareField> Circuit<F> {
194 pub fn new(name: &str, num_rows: usize) -> errors::Result<Self> {
197 if !num_rows.is_power_of_two() {
198 return Err(errors::Error::Protocol {
199 protocol: "circuit",
200 message: "num_rows must be a power of two",
201 });
202 }
203
204 Ok(Self {
205 name: String::from(name),
206 num_rows,
207 cs: ConstraintSystem::new(),
208 physical: Vec::new(),
209 expander: VirtualExpander::new(),
210 pending_run: None,
211 num_virtual: 0,
212 fixed: Vec::new(),
213 boundaries: Vec::new(),
214 buses: Vec::new(),
215 num_public_inputs: 0,
216 inline_chiplets: Vec::new(),
217 inline_hints: Vec::new(),
218 chiplet_defs: Vec::new(),
219 namespaces: Vec::new(),
220 })
221 }
222
223 #[inline(always)]
224 pub fn num_rows(&self) -> usize {
225 self.num_rows
226 }
227
228 pub fn cs(&self) -> &ConstraintSystem<F> {
231 &self.cs
232 }
233
234 pub fn column(&mut self, ty: ColumnType) -> Col {
235 self.columns(1, ty).at(0)
236 }
237
238 pub fn schema(&mut self, layout: &[ColumnType]) -> ColRange {
241 let start = self.num_virtual;
242
243 for &ty in layout {
244 self.column(ty);
245 }
246
247 ColRange {
248 start,
249 count: layout.len(),
250 }
251 }
252
253 pub fn columns(&mut self, count: usize, ty: ColumnType) -> ColRange {
254 let start = self.num_virtual;
255
256 self.physical.extend(core::iter::repeat_n(ty, count));
257
258 self.num_virtual += count;
259
260 match self.pending_run.take() {
261 Some((run_ty, run_count)) if run_ty == ty => {
262 self.pending_run = Some((ty, run_count + count));
263 }
264 prior => {
265 self.flush_run(prior);
266
267 self.pending_run = Some((ty, count));
268 }
269 }
270
271 ColRange { start, count }
272 }
273
274 pub fn pass_through(&mut self, count: usize, storage: ColumnType) -> ColRange {
278 self.flush_pending();
279
280 let cols = ColRange {
281 start: self.num_virtual,
282 count,
283 };
284
285 self.physical.extend(core::iter::repeat_n(storage, count));
286
287 self.num_virtual += count;
288
289 let expander = core::mem::take(&mut self.expander);
290
291 self.expander = expander.pass_through(count, storage);
292
293 cols
294 }
295
296 pub fn expand_bits(&mut self, count: usize, storage: ColumnType) -> Packed {
300 self.flush_pending();
301
302 let phys_start = self.physical.len();
303 let bits_per = storage.byte_size() * 8;
304 let bits = ColRange {
305 start: self.num_virtual,
306 count: count * bits_per,
307 };
308
309 self.physical.extend(core::iter::repeat_n(storage, count));
310
311 self.num_virtual += bits.count;
312
313 let expander = core::mem::take(&mut self.expander);
314
315 self.expander = expander.expand_bits(count, storage);
316
317 Packed {
318 phys_start,
319 count,
320 bits_per,
321 bits,
322 }
323 }
324
325 pub fn reuse_pass_through(&mut self, src: impl Into<PhysRange>) -> ColRange {
329 let src = src.into();
330
331 self.flush_pending();
332
333 let view = ColRange {
334 start: self.num_virtual,
335 count: src.count,
336 };
337
338 self.num_virtual += src.count;
339
340 let expander = core::mem::take(&mut self.expander);
341
342 self.expander = expander.reuse_pass_through(src.start, src.count);
343
344 view
345 }
346
347 pub fn reuse_expand_bits(&mut self, src: impl Into<PhysRange>) -> Packed {
351 let src = src.into();
352
353 self.flush_pending();
354
355 let bits_per = self
356 .physical
357 .get(src.start)
358 .map_or(0, |storage| storage.byte_size() * 8);
359
360 let bits = ColRange {
361 start: self.num_virtual,
362 count: src.count * bits_per,
363 };
364
365 self.num_virtual += bits.count;
366
367 let expander = core::mem::take(&mut self.expander);
368
369 self.expander = expander.reuse_expand_bits(src.start, src.count);
370
371 Packed {
372 phys_start: src.start,
373 count: src.count,
374 bits_per,
375 bits,
376 }
377 }
378
379 pub fn physical(&self, cols: ColRange) -> errors::Result<PhysRange> {
382 let pending = self.pending_run.map_or(0, |(_, count)| count);
383 let flushed = self.num_virtual - pending;
384 let tail = self.physical.len() - pending;
385
386 let whole = |virt: usize| match virt {
387 v if v >= self.num_virtual => None,
388 v if v >= flushed => Some(tail + (v - flushed)),
389 v => self.expander.whole_column(v),
390 };
391
392 let start = whole(cols.start)
393 .filter(|&start| (1..cols.count).all(|i| whole(cols.start + i) == Some(start + i)));
394
395 match start {
396 Some(start) => Ok(PhysRange {
397 start,
398 count: cols.count,
399 }),
400 None => Err(errors::Error::Protocol {
401 protocol: "circuit",
402 message: "columns are not one run of whole committed columns",
403 }),
404 }
405 }
406
407 pub fn fix(&mut self, col: Col, shape: FixedShape<F>) {
410 self.fixed.push(FixedColumn {
411 col_idx: col.index(),
412 shape,
413 });
414 }
415
416 pub fn boundary(&mut self, col: Col, row: usize, value: F) {
419 self.boundaries.push(BoundaryConstraint {
420 col_idx: col.index(),
421 row_idx: row,
422 target: BoundaryTarget::Constant(value),
423 });
424 }
425
426 pub fn call(&mut self, service: &Service, values: &[Col], selector: Col) -> errors::Result<()> {
428 let cols: Vec<usize> = values.iter().map(|c| c.index()).collect();
429 let spec = service.request(&cols, selector.index())?;
430
431 self.buses.push((String::from(service.bus_id), spec));
432
433 Ok(())
434 }
435
436 pub fn bus(&mut self, bus_id: &str, spec: PermutationCheckSpec) {
439 self.buses.push((String::from(bus_id), spec));
440 }
441
442 pub fn mount(&mut self, def: ChipletDef<F>) -> Mounted {
445 self.mount_inner(def, true)
446 }
447
448 pub fn mount_unlinked(&mut self, def: ChipletDef<F>) -> Mounted {
451 self.mount_inner(def, false)
452 }
453
454 pub fn attach(&mut self, def: ChipletDef<F>) {
457 self.chiplet_defs.push(def);
458 }
459
460 pub fn attach_namespaced(
464 &mut self,
465 namespace: &str,
466 defs: Vec<ChipletDef<F>>,
467 external: &[&str],
468 ) -> errors::Result<()> {
469 if namespace.is_empty()
470 || namespace.contains(':')
471 || self.namespaces.iter().any(|(n, _)| n == namespace)
472 {
473 return Err(errors::Error::Protocol {
474 protocol: "circuit",
475 message: "namespace is empty, holds ':' or is already attached",
476 });
477 }
478
479 let unknown = external.iter().any(|&id| {
480 !defs
481 .iter()
482 .any(|def| def.permutation_checks().iter().any(|(bus, _)| bus == id))
483 });
484
485 if unknown {
486 return Err(errors::Error::Protocol {
487 protocol: "circuit",
488 message: "external bus id names no bus of the attached tables",
489 });
490 }
491
492 let exempt: Vec<String> = external.iter().map(|&id| String::from(id)).collect();
493 let start = self.chiplet_defs.len();
494
495 for mut def in defs {
496 def.prefix_bus_ids(namespace, &exempt);
497 self.chiplet_defs.push(def);
498 }
499
500 self.namespaces
501 .push((String::from(namespace), start..self.chiplet_defs.len()));
502
503 Ok(())
504 }
505
506 pub fn publish(&mut self, col: Col, row: usize) -> usize {
510 let slot = self.next_slot();
511
512 self.boundaries.push(BoundaryConstraint::with_public_input(
513 col.index(),
514 row,
515 slot,
516 ));
517
518 slot
519 }
520
521 pub fn compile(mut self) -> errors::Result<CircuitProgram<F>> {
524 self.flush_pending();
525
526 let expander = self.expander.build()?;
527 let width = self.num_virtual;
528 let num_vars = self.num_rows.trailing_zeros() as usize;
529
530 if expander.num_virtual_columns() != width {
531 return Err(errors::Error::Protocol {
532 protocol: "circuit",
533 message: "expander virtual width diverged from declared columns",
534 });
535 }
536
537 let one_to_one = expander.num_virtual_columns() == expander.num_physical_columns();
538 let virtual_layout = expander.virtual_layout().to_vec();
539
540 let ast = self.cs.build();
541
542 for (bus_id, spec) in &self.buses {
543 spec.validate_clock_stitching(bus_id)?;
544
545 for (source, _) in &spec.sources {
546 validate_source_range(source, width)?;
547 }
548 }
549
550 validate_fixed_selectors(&self.buses, &self.fixed)?;
551
552 for bc in &self.boundaries {
553 if bc.col_idx >= width || bc.row_idx >= self.num_rows {
554 return Err(errors::Error::Protocol {
555 protocol: "circuit",
556 message: "boundary constraint outside the trace",
557 });
558 }
559 }
560
561 validate_fixed_columns(&self.fixed, &virtual_layout, Some(num_vars))?;
562
563 for bc in &self.boundaries {
564 if let BoundaryTarget::PublicInput(slot) = bc.target
565 && slot >= self.num_public_inputs
566 {
567 return Err(errors::Error::Protocol {
568 protocol: "circuit",
569 message: "boundary public input slot out of range",
570 });
571 }
572 }
573
574 let chiplet_specs: Vec<Vec<(String, PermutationCheckSpec)>> = self
575 .chiplet_defs
576 .iter()
577 .map(|cd| cd.permutation_checks())
578 .collect();
579
580 for (namespace, group) in &self.namespaces {
581 let mut prefix = namespace.clone();
582 prefix.push_str("::");
583
584 let outside = chiplet_specs
585 .iter()
586 .enumerate()
587 .filter(|(i, _)| !group.contains(i))
588 .flat_map(|(_, specs)| specs)
589 .chain(&self.buses)
590 .any(|(id, _)| id.starts_with(&prefix));
591
592 if outside {
593 return Err(errors::Error::Protocol {
594 protocol: "circuit",
595 message: "bus outside a namespace carries its prefix",
596 });
597 }
598 }
599
600 let endpoints = self
601 .buses
602 .iter()
603 .chain(chiplet_specs.iter().flatten())
604 .map(|(id, s)| (id.as_str(), s));
605
606 validate_bus_set(endpoints)?;
607
608 let mut rank_tables = Vec::with_capacity(1 + self.chiplet_defs.len());
609
610 rank_tables.push(RankTable {
611 specs: &self.buses,
612 fixed: &self.fixed,
613 height: TableHeight::Main(num_vars),
614 });
615
616 for (def, specs) in self.chiplet_defs.iter().zip(&chiplet_specs) {
617 rank_tables.push(RankTable {
618 specs,
619 fixed: def.pins(),
620 height: TableHeight::Chiplet(None),
621 });
622 }
623
624 validate_ordered_emits(&rank_tables)?;
625
626 let table = ChipletDef::from_parts(
627 self.name,
628 width,
629 ast,
630 self.physical,
631 virtual_layout,
632 self.boundaries,
633 self.fixed,
634 (!one_to_one).then_some(expander),
635 self.buses,
636 );
637
638 Ok(CircuitProgram {
639 table,
640 num_public_inputs: self.num_public_inputs,
641 inline_chiplets: self.inline_chiplets,
642 inline_kernels: self.inline_hints,
643 chiplet_defs: self.chiplet_defs,
644 })
645 }
646
647 fn mount_inner(&mut self, def: ChipletDef<F>, link: bool) -> Mounted {
648 self.flush_pending();
649
650 let chiplet_idx = self.inline_chiplets.len();
651 let offset = self.num_virtual;
652 let num_columns = Air::<F>::num_columns(&def);
653
654 let physical = PhysRange {
655 start: self.physical.len(),
656 count: def.column_layout().len(),
657 };
658
659 let mut ast = def.constraint_ast();
660 ast.arena.shift_cells(offset);
661
662 self.inline_hints.push(InlineKernelHint {
663 chiplet_idx,
664 root_offset: self.cs.num_roots(),
665 column_offset: offset,
666 });
667
668 self.cs.merge_ast(ast);
669
670 for mut fc in Air::<F>::fixed_columns(&def) {
671 fc.col_idx += offset;
672
673 self.fixed.push(fc);
674 }
675
676 for mut bc in def.boundary_constraints() {
677 bc.col_idx += offset;
678
679 self.boundaries.push(bc);
680 }
681
682 match def.virtual_expander() {
683 Some(e) => {
684 let expander = core::mem::take(&mut self.expander);
685
686 self.expander = expander.append(e);
687
688 self.physical.extend_from_slice(def.column_layout());
689
690 self.num_virtual += num_columns;
691 }
692 None => {
693 self.schema(def.column_layout());
694 }
695 }
696
697 if link {
698 for (bus_id, mut spec) in def.permutation_checks() {
699 spec.shift_column_indices(offset);
700 self.buses.push((bus_id, spec));
701 }
702 }
703
704 self.inline_chiplets.push(def);
705
706 Mounted {
707 offset,
708 num_columns,
709 physical,
710 }
711 }
712
713 fn next_slot(&mut self) -> usize {
714 let slot = self.num_public_inputs;
715 self.num_public_inputs += 1;
716
717 slot
718 }
719
720 fn flush_pending(&mut self) {
721 let prior = self.pending_run.take();
722 self.flush_run(prior);
723 }
724
725 fn flush_run(&mut self, run: Option<(ColumnType, usize)>) {
726 let Some((ty, count)) = run else {
727 return;
728 };
729
730 let expander = core::mem::take(&mut self.expander);
731
732 self.expander = match ty {
733 ColumnType::Bit => expander.control_bits(count),
734 _ => expander.pass_through(count, ty),
735 };
736 }
737}
738
739#[derive(Clone)]
741pub struct CircuitProgram<F: TowerField> {
742 table: ChipletDef<F>,
743 num_public_inputs: usize,
744 inline_chiplets: Vec<ChipletDef<F>>,
745 inline_kernels: Vec<InlineKernelHint>,
746 chiplet_defs: Vec<ChipletDef<F>>,
747}
748
749impl<F: TowerField> Air<F> for CircuitProgram<F> {
750 fn name(&self) -> String {
751 self.table.name()
752 }
753
754 fn num_columns(&self) -> usize {
755 self.table.num_columns()
756 }
757
758 fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
759 self.table.boundary_constraints()
760 }
761
762 fn column_layout(&self) -> &[ColumnType] {
763 self.table.column_layout()
764 }
765
766 fn virtual_column_layout(&self) -> &[ColumnType] {
767 self.table.virtual_column_layout()
768 }
769
770 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
771 self.table.permutation_checks()
772 }
773
774 fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
775 self.table.fixed_columns()
776 }
777
778 fn virtual_expander(&self) -> Option<&VirtualExpander> {
779 self.table.virtual_expander()
780 }
781
782 fn constraint_ast(&self) -> ConstraintAst<F> {
783 self.table.constraint_ast()
784 }
785
786 fn inline_chiplets(&self) -> errors::Result<Vec<ChipletDef<F>>> {
787 Ok(self.inline_chiplets.clone())
788 }
789
790 fn inline_chiplet_kernels(&self) -> Vec<InlineKernelHint> {
791 self.inline_kernels.clone()
792 }
793}
794
795impl<F: TowerField> Program<F> for CircuitProgram<F> {
796 fn num_public_inputs(&self) -> usize {
797 self.num_public_inputs
798 }
799
800 fn chiplet_defs(&self) -> errors::Result<Vec<ChipletDef<F>>> {
801 Ok(self.chiplet_defs.clone())
802 }
803}
804
805fn validate_source_range(source: &Source, width: usize) -> errors::Result<()> {
806 let out_of_range = match source {
807 Source::Column(idx) | Source::PhaseColumn(idx) => *idx >= width,
808 Source::Columns(indices) => indices.iter().any(|&idx| idx >= width),
809 Source::RowIndexLeBytes(_)
810 | Source::RowIndexByte(_)
811 | Source::Const(_)
812 | Source::EmitRank(_) => false,
813 };
814
815 if out_of_range {
816 return Err(errors::Error::Protocol {
817 protocol: "circuit",
818 message: "bus source column out of range",
819 });
820 }
821
822 Ok(())
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828 use crate::digest::program_id;
829 use crate::permutation::{BusKind, EMIT_RANK_LABEL, ServiceSlot, Side, Source};
830 use alloc::string::ToString;
831 use alloc::vec;
832 use hekate_math::Block128;
833
834 type F = Block128;
835
836 #[derive(Clone)]
837 struct MiniChiplet;
838
839 impl Air<F> for MiniChiplet {
840 fn name(&self) -> String {
841 "MiniChiplet".to_string()
842 }
843
844 fn num_columns(&self) -> usize {
845 2
846 }
847
848 fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
849 vec![BoundaryConstraint::with_constant(0, 0, F::ZERO)]
850 }
851
852 fn column_layout(&self) -> &[ColumnType] {
853 &[ColumnType::B32, ColumnType::Bit]
854 }
855
856 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
857 vec![(
858 "mini_bus".to_string(),
859 PermutationCheckSpec::new(
860 vec![
861 (Source::Column(0), b"k_v"),
862 (Source::RowIndexLeBytes(4), b"k_clk"),
863 ],
864 Some(1),
865 ),
866 )]
867 }
868
869 fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
870 vec![FixedColumn::prefix(1, 1)]
871 }
872
873 fn constraint_ast(&self) -> ConstraintAst<F> {
874 let cs = ConstraintSystem::<F>::new();
875 cs.assert_boolean(cs.col(1));
876
877 cs.build()
878 }
879 }
880
881 #[derive(Clone)]
882 struct Server {
883 calls: usize,
884 spec: PermutationCheckSpec,
885 }
886
887 impl Air<F> for Server {
888 fn name(&self) -> String {
889 "Server".to_string()
890 }
891
892 fn num_columns(&self) -> usize {
893 3
894 }
895
896 fn column_layout(&self) -> &[ColumnType] {
897 &[ColumnType::B32, ColumnType::Bit, ColumnType::B32]
898 }
899
900 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
901 vec![("svc".to_string(), self.spec.clone())]
902 }
903
904 fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
905 vec![FixedColumn {
906 col_idx: 1,
907 shape: two_emit_calls(self.calls),
908 }]
909 }
910
911 fn constraint_ast(&self) -> ConstraintAst<F> {
912 ConstraintSystem::<F>::new().build()
913 }
914 }
915
916 fn mini_def() -> ChipletDef<F> {
917 ChipletDef::from_air(&MiniChiplet).unwrap()
918 }
919
920 fn clocked_spec(key_col: usize, selector: usize) -> PermutationCheckSpec {
921 PermutationCheckSpec::new(
922 vec![
923 (Source::Column(key_col), b"k_v"),
924 (Source::RowIndexLeBytes(4), b"k_clk"),
925 ],
926 Some(selector),
927 )
928 }
929
930 fn ordered_service() -> Service {
931 Service {
932 bus_id: "svc",
933 kind: BusKind::Permutation,
934 slots: vec![ServiceSlot::Value(b"k_v"), ServiceSlot::EmitRank],
935 }
936 }
937
938 fn two_emit_calls(calls: usize) -> FixedShape<F> {
939 FixedShape::Cadence {
940 stride: 4,
941 count: calls,
942 origin: 0,
943 values: vec![F::ONE, F::ZERO, F::ZERO, F::ONE],
944 }
945 }
946
947 fn ordered_host(server: Server) -> errors::Result<CircuitProgram<F>> {
948 let mut cx = Circuit::<F>::new("ordered_host", 16)?;
949
950 let value = cx.column(ColumnType::B32);
951 let sel = cx.column(ColumnType::Bit);
952
953 cx.fix(sel, two_emit_calls(2));
954 cx.call(&ordered_service(), &[value], sel)?;
955 cx.attach(ChipletDef::from_air(&server)?);
956
957 cx.compile()
958 }
959
960 fn rejected_by(res: errors::Result<CircuitProgram<F>>, prefix: &str) -> bool {
961 matches!(res, Err(errors::Error::Protocol { message, .. }) if message.starts_with(prefix))
962 }
963
964 #[test]
965 fn mount_matches_manual_shift() {
966 let mut cx = Circuit::<F>::new("mini_host", 16).unwrap();
967 let host_key = cx.column(ColumnType::B32);
968
969 let cs = cx.cs();
970 cs.constrain(cs.col(host_key.index()) * cs.col(host_key.index()));
971
972 let mounted = cx.mount(mini_def());
973
974 assert_eq!(mounted.offset(), 1);
975
976 let program = cx.compile().unwrap();
977
978 assert_eq!(
979 program.column_layout(),
980 &[ColumnType::B32, ColumnType::B32, ColumnType::Bit]
981 );
982
983 let hints = program.inline_chiplet_kernels();
984
985 assert_eq!(hints.len(), 1);
986 assert_eq!(hints[0].chiplet_idx, 0);
987 assert_eq!(hints[0].root_offset, 1);
988 assert_eq!(hints[0].column_offset, 1);
989
990 let mut manual = {
991 let cs = ConstraintSystem::<F>::new();
992 cs.constrain(cs.col(0) * cs.col(0));
993
994 cs.build()
995 };
996
997 let mut chiplet_ast = Air::<F>::constraint_ast(&MiniChiplet);
998 chiplet_ast.arena.shift_cells(1);
999
1000 manual.merge(chiplet_ast);
1001
1002 assert_eq!(
1003 program.constraint_ast().to_constraints(),
1004 manual.to_constraints()
1005 );
1006
1007 let specs = program.permutation_checks();
1008
1009 assert_eq!(specs.len(), 1);
1010 assert_eq!(specs[0].1.selector, Some(2));
1011 assert_eq!(specs[0].1.sources[0].0, Source::Column(1));
1012
1013 let pins = program.fixed_columns();
1014
1015 assert_eq!(pins.len(), 1);
1016 assert_eq!(pins[0].col_idx, 2);
1017
1018 let boundaries = program.boundary_constraints();
1019
1020 assert_eq!(boundaries.len(), 1);
1021 assert_eq!(boundaries[0].col_idx, 1);
1022 }
1023
1024 #[test]
1025 fn mount_unlinked_keeps_columns_and_drops_endpoint() {
1026 let mut cx = Circuit::<F>::new("isolated_host", 16).unwrap();
1027 let mounted = cx.mount_unlinked(mini_def());
1028
1029 let program = cx.compile().unwrap();
1030
1031 assert_eq!(mounted.offset(), 0);
1032 assert_eq!(program.num_columns(), 2);
1033 assert!(program.permutation_checks().is_empty());
1034 assert_eq!(program.fixed_columns().len(), 1);
1035 }
1036
1037 #[test]
1038 fn plain_columns_elide_expander() {
1039 let mut cx = Circuit::<F>::new("plain", 16).unwrap();
1040 cx.columns(3, ColumnType::B64);
1041 cx.column(ColumnType::Bit);
1042
1043 let program = cx.compile().unwrap();
1044
1045 assert!(program.virtual_expander().is_none());
1046 assert_eq!(program.num_columns(), 4);
1047 assert_eq!(program.column_layout(), program.virtual_column_layout());
1048 }
1049
1050 #[test]
1051 fn publish_assigns_sequential_slots() {
1052 let mut cx = Circuit::<F>::new("slots", 16).unwrap();
1053 let val = cx.column(ColumnType::B32);
1054
1055 let s0 = cx.publish(val, 3);
1056 let s1 = cx.publish(val, 5);
1057
1058 assert_eq!((s0, s1), (0, 1));
1059
1060 let program = cx.compile().unwrap();
1061
1062 assert_eq!(program.num_public_inputs(), 2);
1063
1064 let boundaries = program.boundary_constraints();
1065 assert_eq!(boundaries.len(), 2);
1066 assert_eq!(boundaries[0].target, BoundaryTarget::PublicInput(0));
1067 assert_eq!(boundaries[1].target, BoundaryTarget::PublicInput(1));
1068 }
1069
1070 #[test]
1071 fn compile_rejects_witness_bus_selector() {
1072 let mut cx = Circuit::<F>::new("discipline", 16).unwrap();
1073 let key = cx.column(ColumnType::B32);
1074 let sel = cx.column(ColumnType::Bit);
1075
1076 cx.bus("lone_bus", clocked_spec(key.index(), sel.index()));
1077
1078 assert!(cx.compile().is_err());
1079 }
1080
1081 #[test]
1082 fn compile_keeps_pinned_selector_unrooted() {
1083 let mut cx = Circuit::<F>::new("pinned", 16).unwrap();
1084 let key = cx.column(ColumnType::B32);
1085 let sel = cx.column(ColumnType::Bit);
1086
1087 cx.fix(sel, FixedShape::Sparse(vec![(0, F::ONE)]));
1088 cx.bus("lone_bus", clocked_spec(key.index(), sel.index()));
1089
1090 assert!(cx.compile().unwrap().constraint_ast().roots.is_empty());
1091 }
1092
1093 #[test]
1094 fn compile_rejects_out_of_range_bus_source() {
1095 let mut cx = Circuit::<F>::new("oor", 16).unwrap();
1096 let sel = cx.column(ColumnType::Bit);
1097
1098 cx.bus("bad_bus", clocked_spec(99, sel.index()));
1099
1100 assert!(cx.compile().is_err());
1101 }
1102
1103 #[test]
1104 fn mounted_expander_is_kept_and_offset() {
1105 let expander_def = {
1106 #[derive(Clone)]
1107 struct Packed {
1108 expander: VirtualExpander,
1109 }
1110
1111 impl Air<F> for Packed {
1112 fn num_columns(&self) -> usize {
1113 self.expander.num_virtual_columns()
1114 }
1115
1116 fn column_layout(&self) -> &[ColumnType] {
1117 &[ColumnType::B32, ColumnType::Bit]
1118 }
1119
1120 fn virtual_expander(&self) -> Option<&VirtualExpander> {
1121 Some(&self.expander)
1122 }
1123
1124 fn constraint_ast(&self) -> ConstraintAst<F> {
1125 ConstraintSystem::<F>::new().build()
1126 }
1127 }
1128
1129 ChipletDef::from_air(&Packed {
1130 expander: VirtualExpander::new()
1131 .expand_bits(1, ColumnType::B32)
1132 .control_bits(1)
1133 .build()
1134 .unwrap(),
1135 })
1136 .unwrap()
1137 };
1138
1139 let mut cx = Circuit::<F>::new("packed_host", 16).unwrap();
1140
1141 let host = cx.column(ColumnType::B64);
1142 let mounted = cx.mount(expander_def);
1143
1144 assert_eq!(host.index(), 0);
1145 assert_eq!(mounted.offset(), 1);
1146 assert_eq!(mounted.num_columns(), 33);
1147
1148 let program = cx.compile().unwrap();
1149
1150 assert!(program.virtual_expander().is_some());
1151 assert_eq!(program.num_columns(), 34);
1152 assert_eq!(program.virtual_column_layout()[0], ColumnType::B64);
1153 assert_eq!(program.virtual_column_layout()[1], ColumnType::Bit);
1154 assert_eq!(program.virtual_column_layout()[33], ColumnType::Bit);
1155 }
1156
1157 #[test]
1158 fn mount_keeps_nested_inline_chiplets_on_def() {
1159 let inner = {
1160 let mut cx = Circuit::<F>::new("inner", 16).unwrap();
1161 let own = cx.column(ColumnType::B32);
1162
1163 let cs = cx.cs();
1164 cs.constrain(cs.col(own.index()) * cs.col(own.index()));
1165
1166 cx.mount(mini_def());
1167 cx.compile().unwrap()
1168 };
1169
1170 let def = ChipletDef::from_air(&inner).unwrap();
1171 let nested_hints = vec![InlineKernelHint {
1172 chiplet_idx: 0,
1173 root_offset: 1,
1174 column_offset: 1,
1175 }];
1176
1177 assert_eq!(def.inline_chiplet_kernels(), nested_hints);
1178
1179 let bare = ChipletDef::<F>::from_wire(
1180 Air::<F>::name(&def),
1181 Air::<F>::num_columns(&def),
1182 def.constraint_ast(),
1183 def.column_layout().to_vec(),
1184 def.virtual_column_layout().to_vec(),
1185 def.boundary_constraints(),
1186 Air::<F>::fixed_columns(&def),
1187 def.virtual_expander().cloned(),
1188 def.permutation_checks(),
1189 Vec::new(),
1190 Vec::new(),
1191 )
1192 .unwrap();
1193
1194 let outer = |mounted_def: ChipletDef<F>| {
1195 let mut cx = Circuit::<F>::new("outer", 16).unwrap();
1196 let own = cx.column(ColumnType::B64);
1197
1198 let cs = cx.cs();
1199 cs.constrain(cs.col(own.index()) * cs.col(own.index()));
1200
1201 let mounted = cx.mount(mounted_def);
1202
1203 (mounted, cx.compile().unwrap())
1204 };
1205
1206 let (mounted, program) = outer(def);
1207
1208 assert_eq!(mounted.physical(), PhysRange { start: 1, count: 3 });
1209 assert_eq!(
1210 program.inline_chiplet_kernels(),
1211 vec![InlineKernelHint {
1212 chiplet_idx: 0,
1213 root_offset: 1,
1214 column_offset: 1,
1215 }]
1216 );
1217
1218 let inline_defs = Air::<F>::inline_chiplets(&program).unwrap();
1219
1220 assert_eq!(inline_defs.len(), 1);
1221 assert_eq!(inline_defs[0].inline_hints(), nested_hints);
1222
1223 let mut mini_ast = Air::<F>::constraint_ast(&MiniChiplet);
1224 mini_ast.arena.shift_cells(2);
1225
1226 assert_eq!(
1227 program.constraint_ast().to_constraints()[2..],
1228 mini_ast.to_constraints()[..]
1229 );
1230
1231 assert_eq!(
1232 program_id::<F, _>(&program).unwrap(),
1233 program_id::<F, _>(&outer(bare).1).unwrap()
1234 );
1235
1236 let resnapshot = ChipletDef::from_air(&program).unwrap();
1237
1238 assert_eq!(resnapshot.inline_defs()[0].inline_hints(), nested_hints);
1239 }
1240
1241 #[test]
1242 fn reuse_views_match_hand_built_expander() {
1243 let mut cx = Circuit::<F>::new("views", 16).unwrap();
1244
1245 let lanes = cx.pass_through(3, ColumnType::B64);
1246
1247 cx.pass_through(2, ColumnType::B64);
1248
1249 let flags = cx.schema(&[ColumnType::B16, ColumnType::B32]);
1250 let mounted = cx.mount(mini_def());
1251
1252 let lane_bits = cx.reuse_expand_bits(cx.physical(lanes).unwrap().slice(1, 2));
1253
1254 cx.reuse_expand_bits(cx.physical(flags).unwrap().slice(1, 1));
1255 cx.reuse_expand_bits(mounted.physical().slice(0, 1));
1256 cx.reuse_pass_through(&lane_bits);
1257
1258 let program = cx.compile().unwrap();
1259
1260 let hand = VirtualExpander::new()
1261 .pass_through(3, ColumnType::B64)
1262 .pass_through(2, ColumnType::B64)
1263 .pass_through(1, ColumnType::B16)
1264 .pass_through(1, ColumnType::B32)
1265 .pass_through(1, ColumnType::B32)
1266 .control_bits(1)
1267 .reuse_expand_bits(1, 2)
1268 .reuse_expand_bits(6, 1)
1269 .reuse_expand_bits(7, 1)
1270 .reuse_pass_through(1, 2)
1271 .build()
1272 .unwrap();
1273
1274 let built = program.virtual_expander().unwrap();
1275
1276 assert_eq!(built.expansion_entries(), hand.expansion_entries());
1277 assert_eq!(built.virtual_layout(), hand.virtual_layout());
1278 assert_eq!(program.column_layout().len(), hand.num_physical_columns());
1279 }
1280
1281 #[test]
1282 fn physical_maps_whole_columns_only() {
1283 let mut cx = Circuit::<F>::new("physical", 16).unwrap();
1284
1285 let packed = cx.expand_bits(1, ColumnType::B32);
1286 let pending = cx.columns(2, ColumnType::B64);
1287
1288 assert_eq!(
1289 cx.physical(pending).unwrap(),
1290 PhysRange { start: 1, count: 2 }
1291 );
1292
1293 assert!(cx.physical(packed.bits(0)).is_err());
1294
1295 let view = cx.reuse_pass_through(&packed);
1296
1297 assert_eq!(cx.physical(view).unwrap(), PhysRange { start: 0, count: 1 });
1298 }
1299
1300 #[test]
1301 fn reuse_rejects_range_across_expander_entries() {
1302 let across_schema = {
1303 let mut cx = Circuit::<F>::new("across_schema", 16).unwrap();
1304 let flags = cx.schema(&[ColumnType::B16, ColumnType::B32]);
1305
1306 cx.reuse_pass_through(cx.physical(flags).unwrap());
1307 cx.compile().err()
1308 };
1309
1310 let across_mount = {
1311 let mut cx = Circuit::<F>::new("across_mount", 16).unwrap();
1312 let mounted = cx.mount(mini_def());
1313
1314 cx.reuse_pass_through(mounted.physical());
1315 cx.compile().err()
1316 };
1317
1318 let rejected = Some(errors::Error::Protocol {
1319 protocol: "virtual_expand",
1320 message: "reuse: source columns not found in any single fresh entry",
1321 });
1322
1323 assert_eq!(across_schema, rejected);
1324 assert_eq!(across_mount, rejected);
1325 }
1326
1327 #[test]
1328 fn compile_balances_ordered_bus_sides() {
1329 let responder = |calls| Server {
1330 calls,
1331 spec: ordered_service().respond(&[0], 1).unwrap(),
1332 };
1333
1334 ordered_host(responder(2)).unwrap();
1335
1336 assert!(rejected_by(
1337 ordered_host(responder(3)),
1338 "ordered bus sides emit different counts"
1339 ));
1340 }
1341
1342 #[test]
1343 fn compile_rejects_witness_clock_on_ordered_bus() {
1344 let spec = PermutationCheckSpec::new(
1345 vec![(Source::Column(0), b"k_v"), (Source::Column(2), b"k_clk")],
1346 Some(1),
1347 )
1348 .with_clock_waiver("see circuit.rs: waived to reach the ordered-bus check");
1349
1350 assert!(rejected_by(
1351 ordered_host(Server { calls: 2, spec }),
1352 "ordered bus endpoint has no EmitRank source"
1353 ));
1354 }
1355
1356 #[test]
1357 fn program_id_binds_the_rank_side() {
1358 let program = |first, second| {
1359 let mut cx = Circuit::<F>::new("sides", 16).unwrap();
1360
1361 let value = cx.column(ColumnType::B32);
1362 let sel = cx.column(ColumnType::Bit);
1363
1364 cx.fix(sel, two_emit_calls(2));
1365
1366 for side in [first, second] {
1367 cx.bus(
1368 "svc",
1369 PermutationCheckSpec::new(
1370 vec![
1371 (Source::Column(value.index()), b"k_v"),
1372 (Source::EmitRank(side), EMIT_RANK_LABEL),
1373 ],
1374 Some(sel.index()),
1375 ),
1376 );
1377 }
1378
1379 program_id::<F, _>(&cx.compile().unwrap()).unwrap()
1380 };
1381
1382 assert_ne!(
1383 program(Side::Request, Side::Response),
1384 program(Side::Response, Side::Request)
1385 );
1386 }
1387
1388 #[test]
1389 fn attach_namespaced_renames_all_but_external_buses() {
1390 let server = Server {
1391 calls: 2,
1392 spec: ordered_service().respond(&[0], 1).unwrap(),
1393 };
1394
1395 let mut cx = Circuit::<F>::new("namespaced", 16).unwrap();
1396
1397 let value = cx.column(ColumnType::B32);
1398 let sel = cx.column(ColumnType::Bit);
1399
1400 cx.fix(sel, two_emit_calls(2));
1401 cx.call(&ordered_service(), &[value], sel).unwrap();
1402
1403 let defs = vec![ChipletDef::from_air(&server).unwrap(), mini_def()];
1404
1405 cx.attach_namespaced("grp", defs, &["svc"]).unwrap();
1406
1407 let ids: Vec<String> = cx
1408 .compile()
1409 .unwrap()
1410 .chiplet_defs()
1411 .unwrap()
1412 .iter()
1413 .flat_map(|def| def.permutation_checks())
1414 .map(|(id, _)| id)
1415 .collect();
1416
1417 assert_eq!(ids, ["svc", "grp::mini_bus"]);
1418 }
1419
1420 #[test]
1421 fn attach_namespaced_rejects_empty_and_reused_namespaces() {
1422 let mut cx = Circuit::<F>::new("twice", 16).unwrap();
1423
1424 cx.attach_namespaced("grp", vec![mini_def()], &[]).unwrap();
1425
1426 assert!(cx.attach_namespaced("grp", vec![mini_def()], &[]).is_err());
1427 assert!(cx.attach_namespaced("", vec![mini_def()], &[]).is_err());
1428 }
1429
1430 #[test]
1431 fn attach_namespaced_rejects_nested_namespaces_and_unknown_external_ids() {
1432 let mut cx = Circuit::<F>::new("nested", 16).unwrap();
1433
1434 assert_eq!(
1435 cx.attach_namespaced("a::b", vec![mini_def()], &[]).err(),
1436 Some(errors::Error::Protocol {
1437 protocol: "circuit",
1438 message: "namespace is empty, holds ':' or is already attached",
1439 })
1440 );
1441 assert_eq!(
1442 cx.attach_namespaced("grp", vec![mini_def()], &["svc"])
1443 .err(),
1444 Some(errors::Error::Protocol {
1445 protocol: "circuit",
1446 message: "external bus id names no bus of the attached tables",
1447 })
1448 );
1449 }
1450
1451 #[test]
1452 fn bus_outside_namespace_cannot_carry_its_prefix() {
1453 let mut cx = Circuit::<F>::new("prefixed", 16).unwrap();
1454
1455 let value = cx.column(ColumnType::B32);
1456 let sel = cx.column(ColumnType::Bit);
1457
1458 cx.fix(sel, two_emit_calls(2));
1459 cx.bus("grp::mini_bus", clocked_spec(value.index(), sel.index()));
1460
1461 cx.attach_namespaced("grp", vec![mini_def()], &[]).unwrap();
1462
1463 assert_eq!(
1464 cx.compile().err(),
1465 Some(errors::Error::Protocol {
1466 protocol: "circuit",
1467 message: "bus outside a namespace carries its prefix",
1468 })
1469 );
1470 }
1471}