1use crate::constraint::{
26 BoundaryConstraint, BoundaryTarget, ConstraintAst, ConstraintExpr, ExprId,
27};
28use crate::expander::VirtualExpander;
29use crate::permutation::PermutationCheckSpec;
30use crate::{Air, FixedColumn, ProgramCell, validate_fixed_columns};
31use alloc::boxed::Box;
32use alloc::string::String;
33use alloc::vec::Vec;
34use hekate_core::errors;
35use hekate_core::poly::PolyVariant;
36use hekate_core::trace::{ColumnTrace, ColumnType, Trace, TraceCompatibleField};
37use hekate_math::{Flat, HardwareField, PackableField, TowerField};
38
39pub struct ChipletDef<F: TowerField> {
41 name: String,
42 num_columns: usize,
43 constraint_ast: ConstraintAst<F>,
44 column_layout: Vec<ColumnType>,
45 virtual_column_layout: Vec<ColumnType>,
46 boundary_constraints: Vec<BoundaryConstraint<F>>,
47 fixed_columns: Vec<FixedColumn<F>>,
48 expander: Option<VirtualExpander>,
49 pub permutation_checks: Vec<(String, PermutationCheckSpec)>,
50}
51
52impl<F: TowerField> ChipletDef<F> {
53 pub fn from_air<P: Air<F> + Send + 'static>(p: &P) -> errors::Result<Self>
57 where
58 F: TraceCompatibleField + PackableField + HardwareField + 'static,
59 <F as PackableField>::Packed: Copy + Send + Sync,
60 {
61 let permutation_checks = p.permutation_checks();
62 for (bus_id, spec) in &permutation_checks {
63 spec.validate_clock_stitching(bus_id)?;
64 }
65
66 let constraint_ast = p.constraint_ast();
67 validate_paired_bus_mutex(&permutation_checks, &constraint_ast)?;
68
69 let boundary_constraints = p.boundary_constraints();
70 validate_chiplet_boundaries(&boundary_constraints, p.num_columns())?;
71
72 let fixed_columns = p.fixed_columns();
73 validate_fixed_columns(&fixed_columns, p.virtual_column_layout(), None)?;
74
75 Ok(Self {
76 name: p.name(),
77 num_columns: p.num_columns(),
78 constraint_ast,
79 column_layout: p.column_layout().to_vec(),
80 virtual_column_layout: p.virtual_column_layout().to_vec(),
81 boundary_constraints,
82 fixed_columns,
83 expander: p.virtual_expander().cloned(),
84 permutation_checks,
85 })
86 }
87
88 pub fn prefix_bus_ids(&mut self, prefix: &str, exempt: &[String]) {
91 for (bus_id, _) in &mut self.permutation_checks {
92 if !exempt.contains(bus_id) {
93 let mut prefixed = String::from(prefix);
94 prefixed.push_str("::");
95 prefixed.push_str(bus_id);
96
97 *bus_id = prefixed;
98 }
99 }
100 }
101
102 pub fn expand_variants<'a>(
106 &self,
107 trace: &'a ColumnTrace,
108 ) -> errors::Result<Vec<PolyVariant<'a, F>>>
109 where
110 F: TraceCompatibleField + 'static,
111 {
112 match &self.expander {
113 Some(e) => e.expand_variants(trace, 0),
114 None => trace.get_poly_variants::<F>(),
115 }
116 }
117
118 #[allow(clippy::too_many_arguments)]
121 pub fn from_wire(
122 name: String,
123 num_columns: usize,
124 constraint_ast: ConstraintAst<F>,
125 column_layout: Vec<ColumnType>,
126 virtual_column_layout: Vec<ColumnType>,
127 boundary_constraints: Vec<BoundaryConstraint<F>>,
128 fixed_columns: Vec<FixedColumn<F>>,
129 expander: Option<VirtualExpander>,
130 permutation_checks: Vec<(String, PermutationCheckSpec)>,
131 ) -> errors::Result<Self> {
132 for (bus_id, spec) in &permutation_checks {
133 spec.validate_clock_stitching(bus_id)?;
134 }
135
136 validate_paired_bus_mutex(&permutation_checks, &constraint_ast)?;
137 validate_chiplet_boundaries(&boundary_constraints, num_columns)?;
138
139 let virt_layout = match &expander {
140 Some(e) => e.virtual_layout(),
141 None => virtual_column_layout.as_slice(),
142 };
143
144 validate_fixed_columns(&fixed_columns, virt_layout, None)?;
145
146 Ok(Self {
147 name,
148 num_columns,
149 constraint_ast,
150 column_layout,
151 virtual_column_layout,
152 boundary_constraints,
153 fixed_columns,
154 expander,
155 permutation_checks,
156 })
157 }
158}
159
160impl<F: TowerField> Clone for ChipletDef<F> {
161 fn clone(&self) -> Self {
162 Self {
163 name: self.name.clone(),
164 num_columns: self.num_columns,
165 constraint_ast: self.constraint_ast.clone(),
166 column_layout: self.column_layout.clone(),
167 virtual_column_layout: self.virtual_column_layout.clone(),
168 boundary_constraints: self.boundary_constraints.clone(),
169 fixed_columns: self.fixed_columns.clone(),
170 expander: self.expander.clone(),
171 permutation_checks: self.permutation_checks.clone(),
172 }
173 }
174}
175
176impl<F: TowerField> Air<F> for ChipletDef<F> {
177 fn name(&self) -> String {
178 self.name.clone()
179 }
180
181 fn num_columns(&self) -> usize {
182 self.num_columns
183 }
184
185 fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
186 self.boundary_constraints.clone()
187 }
188
189 fn column_layout(&self) -> &[ColumnType] {
190 &self.column_layout
191 }
192
193 fn virtual_column_layout(&self) -> &[ColumnType] {
194 match &self.expander {
195 Some(e) => e.virtual_layout(),
196 None => &self.virtual_column_layout,
197 }
198 }
199
200 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
201 self.permutation_checks.clone()
202 }
203
204 fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
205 self.fixed_columns.clone()
206 }
207
208 fn virtual_expander(&self) -> Option<&VirtualExpander> {
209 self.expander.as_ref()
210 }
211
212 fn parse_virtual_row(&self, bytes: &[u8], res: &mut Vec<Flat<F>>)
213 where
214 F: TraceCompatibleField,
215 {
216 if let Some(e) = &self.expander {
217 res.clear();
218
219 e.parse_row(bytes, res)
220 .expect("committed row byte length must match physical_row_bytes");
221 return;
222 }
223
224 res.clear();
225
226 let mut offset = 0;
227 for col_type in &self.column_layout {
228 let size = col_type.byte_size();
229 if offset + size <= bytes.len() {
230 res.push(col_type.parse_from_bytes(&bytes[offset..offset + size]));
231 offset += size;
232 }
233 }
234 }
235
236 fn constraint_ast(&self) -> ConstraintAst<F> {
237 self.constraint_ast.clone()
238 }
239}
240
241trait AirFactory<F: TowerField>: Send + Sync {
248 fn build(&self) -> errors::Result<ChipletDef<F>>;
249 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)>;
250 fn clone_box(&self) -> Box<dyn AirFactory<F>>;
251}
252
253impl<F, A> AirFactory<F> for A
254where
255 F: TowerField + TraceCompatibleField + PackableField + HardwareField + 'static,
256 <F as PackableField>::Packed: Copy + Send + Sync,
257 A: Air<F> + Clone + Send + Sync + 'static,
258{
259 fn build(&self) -> errors::Result<ChipletDef<F>> {
260 ChipletDef::from_air(self)
261 }
262
263 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
264 Air::permutation_checks(self)
265 }
266
267 fn clone_box(&self) -> Box<dyn AirFactory<F>> {
268 Box::new(self.clone())
269 }
270}
271
272impl<F: TowerField> Clone for Box<dyn AirFactory<F>> {
273 fn clone(&self) -> Self {
274 self.clone_box()
275 }
276}
277
278struct CompositeEntry<F: TraceCompatibleField> {
279 air: Box<dyn AirFactory<F>>,
280}
281
282impl<F: TraceCompatibleField> Clone for CompositeEntry<F> {
283 fn clone(&self) -> Self {
284 Self {
285 air: self.air.clone_box(),
286 }
287 }
288}
289
290pub struct CompositeChiplet<F: TraceCompatibleField> {
302 name: String,
303 chiplets: Vec<CompositeEntry<F>>,
304 external_bus_ids: Vec<String>,
305 external_buses: Vec<(String, PermutationCheckSpec)>,
306}
307
308impl<F: TraceCompatibleField> Clone for CompositeChiplet<F> {
309 fn clone(&self) -> Self {
310 Self {
311 name: self.name.clone(),
312 chiplets: self.chiplets.clone(),
313 external_bus_ids: self.external_bus_ids.clone(),
314 external_buses: self.external_buses.clone(),
315 }
316 }
317}
318
319impl<F: TraceCompatibleField> CompositeChiplet<F> {
320 pub fn builder(name: &str) -> CompositeChipletBuilder<F> {
323 CompositeChipletBuilder {
324 name: String::from(name),
325 chiplets: Vec::new(),
326 external_bus_ids: Vec::new(),
327 external_buses: Vec::new(),
328 }
329 }
330
331 pub fn flatten_defs(&self) -> errors::Result<Vec<ChipletDef<F>>> {
333 let mut out = Vec::with_capacity(self.chiplets.len());
334 for entry in &self.chiplets {
335 let mut def = entry.air.build()?;
336 def.prefix_bus_ids(&self.name, &self.external_bus_ids);
337
338 out.push(def);
339 }
340
341 Ok(out)
342 }
343
344 pub fn external_buses(&self) -> Vec<(String, PermutationCheckSpec)> {
349 self.external_buses.clone()
350 }
351
352 pub fn len(&self) -> usize {
355 self.chiplets.len()
356 }
357
358 pub fn is_empty(&self) -> bool {
361 self.chiplets.is_empty()
362 }
363
364 pub fn name(&self) -> &str {
366 &self.name
367 }
368}
369
370pub struct CompositeChipletBuilder<F: TraceCompatibleField> {
372 name: String,
373 chiplets: Vec<CompositeEntry<F>>,
374 external_bus_ids: Vec<String>,
375 external_buses: Vec<(String, PermutationCheckSpec)>,
376}
377
378impl<F: TraceCompatibleField> CompositeChipletBuilder<F> {
379 pub fn chiplet<A>(mut self, air: A) -> Self
381 where
382 A: Air<F> + Clone + Send + Sync + 'static,
383 F: TraceCompatibleField + PackableField + HardwareField + 'static,
384 <F as PackableField>::Packed: Copy + Send + Sync,
385 {
386 self.chiplets.push(CompositeEntry { air: Box::new(air) });
387
388 self
389 }
390
391 pub fn external_bus(mut self, bus_id: &str, spec: PermutationCheckSpec) -> Self {
394 self.external_bus_ids.push(String::from(bus_id));
395 self.external_buses.push((String::from(bus_id), spec));
396
397 self
398 }
399
400 pub fn build(self) -> errors::Result<CompositeChiplet<F>> {
408 for (bus_id, spec) in &self.external_buses {
409 spec.validate_clock_stitching(bus_id)?;
410 }
411
412 for entry in &self.chiplets {
413 let checks = entry.air.permutation_checks();
414 for i in 0..checks.len() {
415 for j in (i + 1)..checks.len() {
416 if checks[i].0 == checks[j].0 {
417 continue;
418 }
419
420 if let (Some(sel_i), Some(sel_j)) = (checks[i].1.selector, checks[j].1.selector)
421 && sel_i == sel_j
422 {
423 return Err(errors::Error::Protocol {
424 protocol: "composite_chiplet",
425 message: "different bus_ids share a selector column",
426 });
427 }
428 }
429 }
430 }
431
432 Ok(CompositeChiplet {
433 name: self.name,
434 chiplets: self.chiplets,
435 external_bus_ids: self.external_bus_ids,
436 external_buses: self.external_buses,
437 })
438 }
439}
440
441pub fn compose_chiplet_defs<F: TraceCompatibleField>(
452 composites: &[&CompositeChiplet<F>],
453) -> errors::Result<Vec<ChipletDef<F>>> {
454 for i in 0..composites.len() {
455 for j in (i + 1)..composites.len() {
456 if composites[i].name == composites[j].name {
457 return Err(errors::Error::Protocol {
458 protocol: "composite_chiplet",
459 message: "duplicate composite name in compose_chiplet_defs",
460 });
461 }
462 }
463 }
464
465 let mut defs = Vec::new();
466 for composite in composites {
467 defs.extend(composite.flatten_defs()?);
468 }
469
470 let endpoints = defs
471 .iter()
472 .flat_map(|d| d.permutation_checks.iter().map(|(id, s)| (id.as_str(), s)));
473
474 crate::permutation::validate_bus_set(endpoints)?;
475
476 Ok(defs)
477}
478
479pub fn compose_external_buses<F: TraceCompatibleField>(
482 composites: &[&CompositeChiplet<F>],
483) -> Vec<(String, PermutationCheckSpec)> {
484 let mut buses = Vec::new();
485 for composite in composites {
486 buses.extend(composite.external_buses());
487 }
488
489 buses
490}
491
492pub fn validate_paired_bus_mutex<F: TowerField>(
498 specs: &[(String, PermutationCheckSpec)],
499 ast: &ConstraintAst<F>,
500) -> errors::Result<()> {
501 for (_bus_id, spec) in specs {
502 let (s_send, s_recv) = match (spec.selector, spec.recv_selector) {
503 (Some(send), Some(recv)) => (send, recv),
504 (None, Some(_)) => {
505 return Err(errors::Error::Protocol {
506 protocol: "logup_bus",
507 message: "paired bus has recv_selector without send selector",
508 });
509 }
510 _ => continue,
511 };
512
513 if !ast_contains_mutex_root(ast, s_send, s_recv) {
514 return Err(errors::Error::Protocol {
515 protocol: "logup_bus",
516 message: "paired bus requires `s_send · s_recv = 0` mutex root in the AST",
517 });
518 }
519
520 if !ast_contains_boolean_root(ast, s_send) {
521 return Err(errors::Error::Protocol {
522 protocol: "logup_bus",
523 message: "paired bus requires boolean-assertion root for s_send",
524 });
525 }
526
527 if !ast_contains_boolean_root(ast, s_recv) {
528 return Err(errors::Error::Protocol {
529 protocol: "logup_bus",
530 message: "paired bus requires boolean-assertion root for s_recv",
531 });
532 }
533 }
534
535 Ok(())
536}
537
538fn validate_chiplet_boundaries<F>(
542 boundaries: &[BoundaryConstraint<F>],
543 num_columns: usize,
544) -> errors::Result<()> {
545 for bc in boundaries {
546 if bc.col_idx >= num_columns {
547 return Err(errors::Error::Protocol {
548 protocol: "boundary",
549 message: "chiplet boundary col_idx out of range",
550 });
551 }
552
553 if matches!(bc.target, BoundaryTarget::PublicInput(_)) {
554 return Err(errors::Error::Protocol {
555 protocol: "boundary",
556 message: "chiplet boundaries must use BoundaryTarget::Constant",
557 });
558 }
559 }
560
561 Ok(())
562}
563
564pub fn unconstrained_bit_selectors<'a, F: TowerField>(
570 specs: &'a [(String, PermutationCheckSpec)],
571 ast: &ConstraintAst<F>,
572 virtual_layout: &[ColumnType],
573) -> Vec<(usize, &'a str)> {
574 let mut flagged: Vec<(usize, &str)> = Vec::new();
575 for (bus_id, spec) in specs {
576 if spec.recv_selector.is_some() {
577 continue;
578 }
579
580 let Some(sel) = spec.selector else {
581 continue;
582 };
583
584 if virtual_layout.get(sel) != Some(&ColumnType::Bit) {
585 continue;
586 }
587
588 if !ast_contains_boolean_root(ast, sel) && !flagged.iter().any(|(s, _)| *s == sel) {
589 flagged.push((sel, bus_id.as_str()));
590 }
591 }
592
593 flagged
594}
595
596fn ast_contains_mutex_root<F: TowerField>(
597 ast: &ConstraintAst<F>,
598 s_send: usize,
599 s_recv: usize,
600) -> bool {
601 ast.roots
602 .iter()
603 .any(|root| is_mutex_product(ast, *root, s_send, s_recv))
604}
605
606fn ast_contains_boolean_root<F: TowerField>(ast: &ConstraintAst<F>, col: usize) -> bool {
607 ast.roots
608 .iter()
609 .any(|root| is_boolean_assertion(ast, *root, col))
610}
611
612fn is_boolean_assertion<F: TowerField>(ast: &ConstraintAst<F>, id: ExprId, col: usize) -> bool {
613 let ConstraintExpr::Add(a, b) = ast.arena.get(id) else {
614 return false;
615 };
616
617 matches_boolean_pair(ast, *a, *b, col) || matches_boolean_pair(ast, *b, *a, col)
618}
619
620fn matches_boolean_pair<F: TowerField>(
621 ast: &ConstraintAst<F>,
622 sq_id: ExprId,
623 cell_id: ExprId,
624 col: usize,
625) -> bool {
626 let ConstraintExpr::Mul(x, y) = ast.arena.get(sq_id) else {
627 return false;
628 };
629
630 current_col_idx(ast, *x) == Some(col)
631 && current_col_idx(ast, *y) == Some(col)
632 && current_col_idx(ast, cell_id) == Some(col)
633}
634
635fn is_mutex_product<F: TowerField>(
636 ast: &ConstraintAst<F>,
637 id: ExprId,
638 s_send: usize,
639 s_recv: usize,
640) -> bool {
641 let ConstraintExpr::Mul(a, b) = ast.arena.get(id) else {
642 return false;
643 };
644
645 let lhs = current_col_idx(ast, *a);
646 let rhs = current_col_idx(ast, *b);
647
648 matches!(
649 (lhs, rhs),
650 (Some(x), Some(y)) if (x == s_send && y == s_recv) || (x == s_recv && y == s_send)
651 )
652}
653
654fn current_col_idx<F: TowerField>(ast: &ConstraintAst<F>, id: ExprId) -> Option<usize> {
655 match ast.arena.get(id) {
656 ConstraintExpr::Cell(ProgramCell {
657 col_idx,
658 next_row: false,
659 }) => Some(*col_idx),
660 _ => None,
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use crate::ConstraintAst;
668 use crate::constraint::builder::ConstraintSystem;
669 use crate::define_columns;
670 use crate::permutation::{BusKind, ChallengeLabel, PermutationCheckSpec, Source};
671 use alloc::string::String;
672 use alloc::vec;
673 use hekate_core::trace::ColumnType;
674 use hekate_math::Block128;
675
676 type F = Block128;
677
678 define_columns! {
679 PairedAirCols {
680 KEY: B32,
681 S_SEND: Bit,
682 S_RECV: Bit,
683 }
684 }
685
686 #[derive(Clone)]
687 struct OneBusAir {
688 spec: PermutationCheckSpec,
689 }
690
691 impl Air<F> for OneBusAir {
692 fn num_columns(&self) -> usize {
693 2
694 }
695
696 fn column_layout(&self) -> &[ColumnType] {
697 &[ColumnType::B32, ColumnType::Bit]
698 }
699
700 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
701 vec![("test_bus".into(), self.spec.clone())]
702 }
703
704 fn constraint_ast(&self) -> ConstraintAst<F> {
705 ConstraintSystem::<F>::new().build()
706 }
707 }
708
709 #[derive(Clone)]
710 struct PairedAir {
711 with_mutex: bool,
712 }
713
714 impl Air<F> for PairedAir {
715 fn num_columns(&self) -> usize {
716 PairedAirCols::NUM_COLUMNS
717 }
718
719 fn column_layout(&self) -> &[ColumnType] {
720 static LAYOUT: std::sync::OnceLock<Vec<ColumnType>> = std::sync::OnceLock::new();
721 LAYOUT.get_or_init(PairedAirCols::build_layout)
722 }
723
724 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
725 let sources = vec![
726 (Source::Column(PairedAirCols::KEY), b"k_a" as ChallengeLabel),
727 (Source::RowIndexLeBytes(4), b"k_clk" as ChallengeLabel),
728 ];
729
730 vec![(
731 "paired_test_bus".into(),
732 PermutationCheckSpec::new_paired(
733 sources,
734 PairedAirCols::S_SEND,
735 PairedAirCols::S_RECV,
736 BusKind::Permutation,
737 ),
738 )]
739 }
740
741 fn constraint_ast(&self) -> ConstraintAst<F> {
742 let cs = ConstraintSystem::<F>::new();
743
744 cs.assert_boolean(cs.col(PairedAirCols::S_SEND));
745 cs.assert_boolean(cs.col(PairedAirCols::S_RECV));
746
747 if self.with_mutex {
748 cs.constrain_named(
749 "paired_bus_mutex",
750 cs.col(PairedAirCols::S_SEND) * cs.col(PairedAirCols::S_RECV),
751 );
752 }
753
754 cs.build()
755 }
756 }
757
758 #[derive(Clone)]
759 struct PairedNoBoolAir;
760
761 impl Air<F> for PairedNoBoolAir {
762 fn num_columns(&self) -> usize {
763 PairedAirCols::NUM_COLUMNS
764 }
765
766 fn column_layout(&self) -> &[ColumnType] {
767 static LAYOUT: std::sync::OnceLock<Vec<ColumnType>> = std::sync::OnceLock::new();
768 LAYOUT.get_or_init(PairedAirCols::build_layout)
769 }
770
771 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
772 let sources = vec![
773 (Source::Column(PairedAirCols::KEY), b"k_a" as ChallengeLabel),
774 (Source::RowIndexLeBytes(4), b"k_clk" as ChallengeLabel),
775 ];
776
777 vec![(
778 "paired_test_bus".into(),
779 PermutationCheckSpec::new_paired(
780 sources,
781 PairedAirCols::S_SEND,
782 PairedAirCols::S_RECV,
783 BusKind::Permutation,
784 ),
785 )]
786 }
787
788 fn constraint_ast(&self) -> ConstraintAst<F> {
789 let cs = ConstraintSystem::<F>::new();
790
791 cs.constrain_named(
792 "paired_bus_mutex",
793 cs.col(PairedAirCols::S_SEND) * cs.col(PairedAirCols::S_RECV),
794 );
795
796 cs.build()
797 }
798 }
799
800 fn key_only() -> Vec<(Source, ChallengeLabel)> {
801 vec![(Source::Column(0), b"k_a")]
802 }
803
804 fn key_with_clock() -> Vec<(Source, ChallengeLabel)> {
805 vec![
806 (Source::Column(0), b"k_a"),
807 (Source::RowIndexLeBytes(4), b"k_clk"),
808 ]
809 }
810
811 fn snapshot(spec: PermutationCheckSpec) -> errors::Result<ChipletDef<F>> {
812 ChipletDef::from_air(&OneBusAir { spec })
813 }
814
815 fn assert_logup_bus_err<T>(res: errors::Result<T>) {
816 match res {
817 Err(errors::Error::Protocol { protocol, .. }) => {
818 assert_eq!(protocol, "logup_bus");
819 }
820 Ok(_) => panic!("expected Err(Protocol {{ protocol: \"logup_bus\", .. }})"),
821 Err(other) => panic!("expected Err(Protocol), got {:?}", other),
822 }
823 }
824
825 #[test]
826 fn def_rejects_permutation_without_clock() {
827 let spec = PermutationCheckSpec::new(key_only(), Some(1));
828 assert_logup_bus_err(snapshot(spec));
829 }
830
831 #[test]
832 fn def_rejects_permutation_with_empty_waiver() {
833 let spec = PermutationCheckSpec::new(key_only(), Some(1)).with_clock_waiver("");
834 assert_logup_bus_err(snapshot(spec));
835 }
836
837 #[test]
838 fn def_rejects_permutation_with_clock_and_waiver() {
839 let spec =
840 PermutationCheckSpec::new(key_with_clock(), Some(1)).with_clock_waiver("redundant");
841 assert_logup_bus_err(snapshot(spec));
842 }
843
844 #[test]
845 fn def_rejects_lookup_with_waiver() {
846 let spec = PermutationCheckSpec::new_lookup(key_only(), Some(1)).with_clock_waiver("nope");
847 assert_logup_bus_err(snapshot(spec));
848 }
849
850 #[test]
851 fn def_accepts_permutation_with_row_index() {
852 let spec = PermutationCheckSpec::new(key_with_clock(), Some(1));
853 snapshot(spec).expect("permutation bus with row-index source must accept");
854 }
855
856 #[test]
857 fn def_accepts_permutation_with_clock_waiver() {
858 let spec = PermutationCheckSpec::new(key_only(), Some(1))
859 .with_clock_waiver("see foo.rs:42: structurally unique by AIR body");
860 snapshot(spec).expect("permutation bus with non-empty clock_waiver must accept");
861 }
862
863 #[test]
864 fn def_accepts_lookup_without_clock() {
865 let spec = PermutationCheckSpec::new_lookup(key_only(), Some(1));
866 snapshot(spec).expect("lookup bus without clock must accept");
867 }
868
869 #[test]
870 fn def_rejects_permutation_with_too_short_waiver() {
871 let spec = PermutationCheckSpec::new(key_only(), Some(1)).with_clock_waiver("see x.rs");
872 assert_logup_bus_err(snapshot(spec));
873 }
874
875 #[test]
876 fn def_rejects_permutation_with_missing_see_citation() {
877 let spec = PermutationCheckSpec::new(key_only(), Some(1))
878 .with_clock_waiver("structurally unique by AIR body but no file citation prefix here");
879 assert_logup_bus_err(snapshot(spec));
880 }
881
882 #[test]
883 fn paired_bus_emits_mutex_and_boolean_assertions() {
884 let cs = ConstraintSystem::<F>::new();
885
886 cs.assert_paired_bus_mutex(PairedAirCols::S_SEND, PairedAirCols::S_RECV);
887
888 let ast = cs.build();
889
890 let labels: Vec<_> = ast.labels.iter().filter_map(|l| *l).collect();
891
892 assert_eq!(
893 labels.iter().filter(|l| **l == "boolean").count(),
894 2,
895 "gadget must emit two boolean assertions"
896 );
897 assert_eq!(
898 labels.iter().filter(|l| **l == "paired_bus_mutex").count(),
899 1,
900 "gadget must emit exactly one mutex root"
901 );
902 }
903
904 #[test]
905 fn paired_bus_shares_cell_nodes() {
906 let cs = ConstraintSystem::<F>::new();
907
908 let send_first = cs.col(PairedAirCols::S_SEND);
909 let recv_first = cs.col(PairedAirCols::S_RECV);
910
911 cs.assert_paired_bus_mutex(PairedAirCols::S_SEND, PairedAirCols::S_RECV);
912
913 let send_again = cs.col(PairedAirCols::S_SEND);
914 let recv_again = cs.col(PairedAirCols::S_RECV);
915
916 assert_eq!(send_first.id, send_again.id, "S_SEND must dedup");
917 assert_eq!(recv_first.id, recv_again.id, "S_RECV must dedup");
918 }
919
920 #[test]
921 fn chiplet_def_rejects_paired_spec_without_mutex() {
922 let bad = PairedAir { with_mutex: false };
923 assert_logup_bus_err(ChipletDef::<F>::from_air(&bad));
924 }
925
926 #[test]
927 fn chiplet_def_accepts_paired_spec_with_mutex() {
928 let good = PairedAir { with_mutex: true };
929 ChipletDef::<F>::from_air(&good).expect("paired AIR with mutex must snapshot");
930 }
931
932 #[test]
933 fn chiplet_def_rejects_paired_spec_without_boolean_roots() {
934 assert_logup_bus_err(ChipletDef::<F>::from_air(&PairedNoBoolAir));
935 }
936
937 #[test]
938 fn validator_rejects_recv_selector_without_send_selector() {
939 let spec = PermutationCheckSpec {
940 sources: vec![
941 (Source::Column(PairedAirCols::KEY), b"k_a" as ChallengeLabel),
942 (Source::RowIndexLeBytes(4), b"k_clk" as ChallengeLabel),
943 ],
944 selector: None,
945 recv_selector: Some(PairedAirCols::S_RECV),
946 kind: BusKind::Permutation,
947 clock_waiver: None,
948 };
949
950 let ast = ConstraintSystem::<F>::new().build();
951
952 assert_logup_bus_err(validate_paired_bus_mutex(
953 &[("asym_bus".into(), spec)],
954 &ast,
955 ));
956 }
957
958 #[test]
959 fn flags_bit_selector_without_boolean_root() {
960 let ast = ConstraintSystem::<F>::new().build();
961 let layout = vec![ColumnType::B32, ColumnType::Bit];
962 let specs = vec![(
963 String::from("bus"),
964 PermutationCheckSpec::new(vec![(Source::Column(0), b"k" as ChallengeLabel)], Some(1)),
965 )];
966
967 assert_eq!(
968 unconstrained_bit_selectors(&specs, &ast, &layout),
969 vec![(1, "bus")]
970 );
971 }
972
973 #[test]
974 fn boolean_root_clears_bit_selector() {
975 let cs = ConstraintSystem::<F>::new();
976 let sel = cs.col(1);
977 cs.assert_boolean(sel);
978 let ast = cs.build();
979
980 let layout = vec![ColumnType::B32, ColumnType::Bit];
981 let specs = vec![(
982 String::from("bus"),
983 PermutationCheckSpec::new(vec![(Source::Column(0), b"k" as ChallengeLabel)], Some(1)),
984 )];
985
986 assert!(unconstrained_bit_selectors(&specs, &ast, &layout).is_empty());
987 }
988}