1use crate::constraint::{
26 BoundaryConstraint, BoundaryTarget, ConstraintAst, ConstraintExpr, ExprId,
27};
28use crate::expander::VirtualExpander;
29use crate::permutation::PermutationCheckSpec;
30use crate::{Air, LagrangePin, ProgramCell, validate_lagrange_pins};
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 lagrange_pins: Vec<LagrangePin>,
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 lagrange_pins = p.lagrange_pinned_columns();
73 validate_lagrange_pins(&lagrange_pins, p.num_columns(), 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 lagrange_pins,
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 lagrange_pins: Vec<LagrangePin>,
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 validate_lagrange_pins(&lagrange_pins, num_columns, None)?;
139
140 Ok(Self {
141 name,
142 num_columns,
143 constraint_ast,
144 column_layout,
145 virtual_column_layout,
146 boundary_constraints,
147 lagrange_pins,
148 expander,
149 permutation_checks,
150 })
151 }
152}
153
154impl<F: TowerField> Clone for ChipletDef<F> {
155 fn clone(&self) -> Self {
156 Self {
157 name: self.name.clone(),
158 num_columns: self.num_columns,
159 constraint_ast: self.constraint_ast.clone(),
160 column_layout: self.column_layout.clone(),
161 virtual_column_layout: self.virtual_column_layout.clone(),
162 boundary_constraints: self.boundary_constraints.clone(),
163 lagrange_pins: self.lagrange_pins.clone(),
164 expander: self.expander.clone(),
165 permutation_checks: self.permutation_checks.clone(),
166 }
167 }
168}
169
170impl<F: TowerField> Air<F> for ChipletDef<F> {
171 fn name(&self) -> String {
172 self.name.clone()
173 }
174
175 fn num_columns(&self) -> usize {
176 self.num_columns
177 }
178
179 fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
180 self.boundary_constraints.clone()
181 }
182
183 fn column_layout(&self) -> &[ColumnType] {
184 &self.column_layout
185 }
186
187 fn virtual_column_layout(&self) -> &[ColumnType] {
188 match &self.expander {
189 Some(e) => e.virtual_layout(),
190 None => &self.virtual_column_layout,
191 }
192 }
193
194 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
195 self.permutation_checks.clone()
196 }
197
198 fn lagrange_pinned_columns(&self) -> Vec<LagrangePin> {
199 self.lagrange_pins.clone()
200 }
201
202 fn virtual_expander(&self) -> Option<&VirtualExpander> {
203 self.expander.as_ref()
204 }
205
206 fn parse_virtual_row(&self, bytes: &[u8], res: &mut Vec<Flat<F>>)
207 where
208 F: TraceCompatibleField,
209 {
210 if let Some(e) = &self.expander {
211 res.clear();
212
213 e.parse_row(bytes, res)
214 .expect("committed row byte length must match physical_row_bytes");
215 return;
216 }
217
218 res.clear();
219
220 let mut offset = 0;
221 for col_type in &self.column_layout {
222 let size = col_type.byte_size();
223 if offset + size <= bytes.len() {
224 res.push(col_type.parse_from_bytes(&bytes[offset..offset + size]));
225 offset += size;
226 }
227 }
228 }
229
230 fn constraint_ast(&self) -> ConstraintAst<F> {
231 self.constraint_ast.clone()
232 }
233}
234
235trait AirFactory<F: TowerField>: Send + Sync {
242 fn build(&self) -> errors::Result<ChipletDef<F>>;
243 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)>;
244 fn clone_box(&self) -> Box<dyn AirFactory<F>>;
245}
246
247impl<F, A> AirFactory<F> for A
248where
249 F: TowerField + TraceCompatibleField + PackableField + HardwareField + 'static,
250 <F as PackableField>::Packed: Copy + Send + Sync,
251 A: Air<F> + Clone + Send + Sync + 'static,
252{
253 fn build(&self) -> errors::Result<ChipletDef<F>> {
254 ChipletDef::from_air(self)
255 }
256
257 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
258 Air::permutation_checks(self)
259 }
260
261 fn clone_box(&self) -> Box<dyn AirFactory<F>> {
262 Box::new(self.clone())
263 }
264}
265
266impl<F: TowerField> Clone for Box<dyn AirFactory<F>> {
267 fn clone(&self) -> Self {
268 self.clone_box()
269 }
270}
271
272struct CompositeEntry<F: TraceCompatibleField> {
273 air: Box<dyn AirFactory<F>>,
274}
275
276impl<F: TraceCompatibleField> Clone for CompositeEntry<F> {
277 fn clone(&self) -> Self {
278 Self {
279 air: self.air.clone_box(),
280 }
281 }
282}
283
284pub struct CompositeChiplet<F: TraceCompatibleField> {
296 name: String,
297 chiplets: Vec<CompositeEntry<F>>,
298 external_bus_ids: Vec<String>,
299 external_buses: Vec<(String, PermutationCheckSpec)>,
300}
301
302impl<F: TraceCompatibleField> Clone for CompositeChiplet<F> {
303 fn clone(&self) -> Self {
304 Self {
305 name: self.name.clone(),
306 chiplets: self.chiplets.clone(),
307 external_bus_ids: self.external_bus_ids.clone(),
308 external_buses: self.external_buses.clone(),
309 }
310 }
311}
312
313impl<F: TraceCompatibleField> CompositeChiplet<F> {
314 pub fn builder(name: &str) -> CompositeChipletBuilder<F> {
317 CompositeChipletBuilder {
318 name: String::from(name),
319 chiplets: Vec::new(),
320 external_bus_ids: Vec::new(),
321 external_buses: Vec::new(),
322 }
323 }
324
325 pub fn flatten_defs(&self) -> errors::Result<Vec<ChipletDef<F>>> {
327 let mut out = Vec::with_capacity(self.chiplets.len());
328 for entry in &self.chiplets {
329 let mut def = entry.air.build()?;
330 def.prefix_bus_ids(&self.name, &self.external_bus_ids);
331
332 out.push(def);
333 }
334
335 Ok(out)
336 }
337
338 pub fn external_buses(&self) -> Vec<(String, PermutationCheckSpec)> {
343 self.external_buses.clone()
344 }
345
346 pub fn len(&self) -> usize {
349 self.chiplets.len()
350 }
351
352 pub fn is_empty(&self) -> bool {
355 self.chiplets.is_empty()
356 }
357
358 pub fn name(&self) -> &str {
360 &self.name
361 }
362}
363
364pub struct CompositeChipletBuilder<F: TraceCompatibleField> {
366 name: String,
367 chiplets: Vec<CompositeEntry<F>>,
368 external_bus_ids: Vec<String>,
369 external_buses: Vec<(String, PermutationCheckSpec)>,
370}
371
372impl<F: TraceCompatibleField> CompositeChipletBuilder<F> {
373 pub fn chiplet<A>(mut self, air: A) -> Self
375 where
376 A: Air<F> + Clone + Send + Sync + 'static,
377 F: TraceCompatibleField + PackableField + HardwareField + 'static,
378 <F as PackableField>::Packed: Copy + Send + Sync,
379 {
380 self.chiplets.push(CompositeEntry { air: Box::new(air) });
381
382 self
383 }
384
385 pub fn external_bus(mut self, bus_id: &str, spec: PermutationCheckSpec) -> Self {
388 self.external_bus_ids.push(String::from(bus_id));
389 self.external_buses.push((String::from(bus_id), spec));
390
391 self
392 }
393
394 pub fn build(self) -> errors::Result<CompositeChiplet<F>> {
402 for (bus_id, spec) in &self.external_buses {
403 spec.validate_clock_stitching(bus_id)?;
404 }
405
406 for entry in &self.chiplets {
407 let checks = entry.air.permutation_checks();
408 for i in 0..checks.len() {
409 for j in (i + 1)..checks.len() {
410 if checks[i].0 == checks[j].0 {
411 continue;
412 }
413
414 if let (Some(sel_i), Some(sel_j)) = (checks[i].1.selector, checks[j].1.selector)
415 && sel_i == sel_j
416 {
417 return Err(errors::Error::Protocol {
418 protocol: "composite_chiplet",
419 message: "different bus_ids share a selector column",
420 });
421 }
422 }
423 }
424 }
425
426 Ok(CompositeChiplet {
427 name: self.name,
428 chiplets: self.chiplets,
429 external_bus_ids: self.external_bus_ids,
430 external_buses: self.external_buses,
431 })
432 }
433}
434
435pub fn compose_chiplet_defs<F: TraceCompatibleField>(
446 composites: &[&CompositeChiplet<F>],
447) -> errors::Result<Vec<ChipletDef<F>>> {
448 for i in 0..composites.len() {
449 for j in (i + 1)..composites.len() {
450 if composites[i].name == composites[j].name {
451 return Err(errors::Error::Protocol {
452 protocol: "composite_chiplet",
453 message: "duplicate composite name in compose_chiplet_defs",
454 });
455 }
456 }
457 }
458
459 let mut defs = Vec::new();
460 for composite in composites {
461 defs.extend(composite.flatten_defs()?);
462 }
463
464 let endpoints = defs
465 .iter()
466 .flat_map(|d| d.permutation_checks.iter().map(|(id, s)| (id.as_str(), s)));
467
468 crate::permutation::validate_bus_set(endpoints)?;
469
470 Ok(defs)
471}
472
473pub fn compose_external_buses<F: TraceCompatibleField>(
476 composites: &[&CompositeChiplet<F>],
477) -> Vec<(String, PermutationCheckSpec)> {
478 let mut buses = Vec::new();
479 for composite in composites {
480 buses.extend(composite.external_buses());
481 }
482
483 buses
484}
485
486pub fn validate_paired_bus_mutex<F: TowerField>(
492 specs: &[(String, PermutationCheckSpec)],
493 ast: &ConstraintAst<F>,
494) -> errors::Result<()> {
495 for (_bus_id, spec) in specs {
496 let (s_send, s_recv) = match (spec.selector, spec.recv_selector) {
497 (Some(send), Some(recv)) => (send, recv),
498 (None, Some(_)) => {
499 return Err(errors::Error::Protocol {
500 protocol: "logup_bus",
501 message: "paired bus has recv_selector without send selector",
502 });
503 }
504 _ => continue,
505 };
506
507 if !ast_contains_mutex_root(ast, s_send, s_recv) {
508 return Err(errors::Error::Protocol {
509 protocol: "logup_bus",
510 message: "paired bus requires `s_send ยท s_recv = 0` mutex root in the AST",
511 });
512 }
513
514 if !ast_contains_boolean_root(ast, s_send) {
515 return Err(errors::Error::Protocol {
516 protocol: "logup_bus",
517 message: "paired bus requires boolean-assertion root for s_send",
518 });
519 }
520
521 if !ast_contains_boolean_root(ast, s_recv) {
522 return Err(errors::Error::Protocol {
523 protocol: "logup_bus",
524 message: "paired bus requires boolean-assertion root for s_recv",
525 });
526 }
527 }
528
529 Ok(())
530}
531
532fn validate_chiplet_boundaries<F>(
536 boundaries: &[BoundaryConstraint<F>],
537 num_columns: usize,
538) -> errors::Result<()> {
539 for bc in boundaries {
540 if bc.col_idx >= num_columns {
541 return Err(errors::Error::Protocol {
542 protocol: "boundary",
543 message: "chiplet boundary col_idx out of range",
544 });
545 }
546
547 if matches!(bc.target, BoundaryTarget::PublicInput(_)) {
548 return Err(errors::Error::Protocol {
549 protocol: "boundary",
550 message: "chiplet boundaries must use BoundaryTarget::Constant",
551 });
552 }
553 }
554
555 Ok(())
556}
557
558fn ast_contains_mutex_root<F: TowerField>(
559 ast: &ConstraintAst<F>,
560 s_send: usize,
561 s_recv: usize,
562) -> bool {
563 ast.roots
564 .iter()
565 .any(|root| is_mutex_product(ast, *root, s_send, s_recv))
566}
567
568fn ast_contains_boolean_root<F: TowerField>(ast: &ConstraintAst<F>, col: usize) -> bool {
569 ast.roots
570 .iter()
571 .any(|root| is_boolean_assertion(ast, *root, col))
572}
573
574fn is_boolean_assertion<F: TowerField>(ast: &ConstraintAst<F>, id: ExprId, col: usize) -> bool {
575 let ConstraintExpr::Add(a, b) = ast.arena.get(id) else {
576 return false;
577 };
578
579 matches_boolean_pair(ast, *a, *b, col) || matches_boolean_pair(ast, *b, *a, col)
580}
581
582fn matches_boolean_pair<F: TowerField>(
583 ast: &ConstraintAst<F>,
584 sq_id: ExprId,
585 cell_id: ExprId,
586 col: usize,
587) -> bool {
588 let ConstraintExpr::Mul(x, y) = ast.arena.get(sq_id) else {
589 return false;
590 };
591
592 current_col_idx(ast, *x) == Some(col)
593 && current_col_idx(ast, *y) == Some(col)
594 && current_col_idx(ast, cell_id) == Some(col)
595}
596
597fn is_mutex_product<F: TowerField>(
598 ast: &ConstraintAst<F>,
599 id: ExprId,
600 s_send: usize,
601 s_recv: usize,
602) -> bool {
603 let ConstraintExpr::Mul(a, b) = ast.arena.get(id) else {
604 return false;
605 };
606
607 let lhs = current_col_idx(ast, *a);
608 let rhs = current_col_idx(ast, *b);
609
610 matches!(
611 (lhs, rhs),
612 (Some(x), Some(y)) if (x == s_send && y == s_recv) || (x == s_recv && y == s_send)
613 )
614}
615
616fn current_col_idx<F: TowerField>(ast: &ConstraintAst<F>, id: ExprId) -> Option<usize> {
617 match ast.arena.get(id) {
618 ConstraintExpr::Cell(ProgramCell {
619 col_idx,
620 next_row: false,
621 }) => Some(*col_idx),
622 _ => None,
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use crate::ConstraintAst;
630 use crate::constraint::builder::ConstraintSystem;
631 use crate::define_columns;
632 use crate::permutation::{BusKind, ChallengeLabel, PermutationCheckSpec, Source};
633 use alloc::string::String;
634 use alloc::vec;
635 use hekate_core::trace::ColumnType;
636 use hekate_math::Block128;
637
638 type F = Block128;
639
640 define_columns! {
641 PairedAirCols {
642 KEY: B32,
643 S_SEND: Bit,
644 S_RECV: Bit,
645 }
646 }
647
648 #[derive(Clone)]
649 struct OneBusAir {
650 spec: PermutationCheckSpec,
651 }
652
653 impl Air<F> for OneBusAir {
654 fn num_columns(&self) -> usize {
655 2
656 }
657
658 fn column_layout(&self) -> &[ColumnType] {
659 &[ColumnType::B32, ColumnType::Bit]
660 }
661
662 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
663 vec![("test_bus".into(), self.spec.clone())]
664 }
665
666 fn constraint_ast(&self) -> ConstraintAst<F> {
667 ConstraintSystem::<F>::new().build()
668 }
669 }
670
671 #[derive(Clone)]
672 struct PairedAir {
673 with_mutex: bool,
674 }
675
676 impl Air<F> for PairedAir {
677 fn num_columns(&self) -> usize {
678 PairedAirCols::NUM_COLUMNS
679 }
680
681 fn column_layout(&self) -> &[ColumnType] {
682 static LAYOUT: std::sync::OnceLock<Vec<ColumnType>> = std::sync::OnceLock::new();
683 LAYOUT.get_or_init(PairedAirCols::build_layout)
684 }
685
686 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
687 let sources = vec![
688 (Source::Column(PairedAirCols::KEY), b"k_a" as ChallengeLabel),
689 (Source::RowIndexLeBytes(4), b"k_clk" as ChallengeLabel),
690 ];
691
692 vec![(
693 "paired_test_bus".into(),
694 PermutationCheckSpec::new_paired(
695 sources,
696 PairedAirCols::S_SEND,
697 PairedAirCols::S_RECV,
698 BusKind::Permutation,
699 ),
700 )]
701 }
702
703 fn constraint_ast(&self) -> ConstraintAst<F> {
704 let cs = ConstraintSystem::<F>::new();
705
706 cs.assert_boolean(cs.col(PairedAirCols::S_SEND));
707 cs.assert_boolean(cs.col(PairedAirCols::S_RECV));
708
709 if self.with_mutex {
710 cs.constrain_named(
711 "paired_bus_mutex",
712 cs.col(PairedAirCols::S_SEND) * cs.col(PairedAirCols::S_RECV),
713 );
714 }
715
716 cs.build()
717 }
718 }
719
720 #[derive(Clone)]
721 struct PairedNoBoolAir;
722
723 impl Air<F> for PairedNoBoolAir {
724 fn num_columns(&self) -> usize {
725 PairedAirCols::NUM_COLUMNS
726 }
727
728 fn column_layout(&self) -> &[ColumnType] {
729 static LAYOUT: std::sync::OnceLock<Vec<ColumnType>> = std::sync::OnceLock::new();
730 LAYOUT.get_or_init(PairedAirCols::build_layout)
731 }
732
733 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
734 let sources = vec![
735 (Source::Column(PairedAirCols::KEY), b"k_a" as ChallengeLabel),
736 (Source::RowIndexLeBytes(4), b"k_clk" as ChallengeLabel),
737 ];
738
739 vec![(
740 "paired_test_bus".into(),
741 PermutationCheckSpec::new_paired(
742 sources,
743 PairedAirCols::S_SEND,
744 PairedAirCols::S_RECV,
745 BusKind::Permutation,
746 ),
747 )]
748 }
749
750 fn constraint_ast(&self) -> ConstraintAst<F> {
751 let cs = ConstraintSystem::<F>::new();
752
753 cs.constrain_named(
754 "paired_bus_mutex",
755 cs.col(PairedAirCols::S_SEND) * cs.col(PairedAirCols::S_RECV),
756 );
757
758 cs.build()
759 }
760 }
761
762 fn key_only() -> Vec<(Source, ChallengeLabel)> {
763 vec![(Source::Column(0), b"k_a")]
764 }
765
766 fn key_with_clock() -> Vec<(Source, ChallengeLabel)> {
767 vec![
768 (Source::Column(0), b"k_a"),
769 (Source::RowIndexLeBytes(4), b"k_clk"),
770 ]
771 }
772
773 fn snapshot(spec: PermutationCheckSpec) -> errors::Result<ChipletDef<F>> {
774 ChipletDef::from_air(&OneBusAir { spec })
775 }
776
777 fn assert_logup_bus_err<T>(res: errors::Result<T>) {
778 match res {
779 Err(errors::Error::Protocol { protocol, .. }) => {
780 assert_eq!(protocol, "logup_bus");
781 }
782 Ok(_) => panic!("expected Err(Protocol {{ protocol: \"logup_bus\", .. }})"),
783 Err(other) => panic!("expected Err(Protocol), got {:?}", other),
784 }
785 }
786
787 #[test]
788 fn def_rejects_permutation_without_clock() {
789 let spec = PermutationCheckSpec::new(key_only(), Some(1));
790 assert_logup_bus_err(snapshot(spec));
791 }
792
793 #[test]
794 fn def_rejects_permutation_with_empty_waiver() {
795 let spec = PermutationCheckSpec::new(key_only(), Some(1)).with_clock_waiver("");
796 assert_logup_bus_err(snapshot(spec));
797 }
798
799 #[test]
800 fn def_rejects_permutation_with_clock_and_waiver() {
801 let spec =
802 PermutationCheckSpec::new(key_with_clock(), Some(1)).with_clock_waiver("redundant");
803 assert_logup_bus_err(snapshot(spec));
804 }
805
806 #[test]
807 fn def_rejects_lookup_with_waiver() {
808 let spec = PermutationCheckSpec::new_lookup(key_only(), Some(1)).with_clock_waiver("nope");
809 assert_logup_bus_err(snapshot(spec));
810 }
811
812 #[test]
813 fn def_accepts_permutation_with_row_index() {
814 let spec = PermutationCheckSpec::new(key_with_clock(), Some(1));
815 snapshot(spec).expect("permutation bus with row-index source must accept");
816 }
817
818 #[test]
819 fn def_accepts_permutation_with_clock_waiver() {
820 let spec = PermutationCheckSpec::new(key_only(), Some(1))
821 .with_clock_waiver("see foo.rs:42: structurally unique by AIR body");
822 snapshot(spec).expect("permutation bus with non-empty clock_waiver must accept");
823 }
824
825 #[test]
826 fn def_accepts_lookup_without_clock() {
827 let spec = PermutationCheckSpec::new_lookup(key_only(), Some(1));
828 snapshot(spec).expect("lookup bus without clock must accept");
829 }
830
831 #[test]
832 fn def_rejects_permutation_with_too_short_waiver() {
833 let spec = PermutationCheckSpec::new(key_only(), Some(1)).with_clock_waiver("see x.rs");
834 assert_logup_bus_err(snapshot(spec));
835 }
836
837 #[test]
838 fn def_rejects_permutation_with_missing_see_citation() {
839 let spec = PermutationCheckSpec::new(key_only(), Some(1))
840 .with_clock_waiver("structurally unique by AIR body but no file citation prefix here");
841 assert_logup_bus_err(snapshot(spec));
842 }
843
844 #[test]
845 fn paired_bus_emits_mutex_and_boolean_assertions() {
846 let cs = ConstraintSystem::<F>::new();
847
848 cs.assert_paired_bus_mutex(PairedAirCols::S_SEND, PairedAirCols::S_RECV);
849
850 let ast = cs.build();
851
852 let labels: Vec<_> = ast.labels.iter().filter_map(|l| *l).collect();
853
854 assert_eq!(
855 labels.iter().filter(|l| **l == "boolean").count(),
856 2,
857 "gadget must emit two boolean assertions"
858 );
859 assert_eq!(
860 labels.iter().filter(|l| **l == "paired_bus_mutex").count(),
861 1,
862 "gadget must emit exactly one mutex root"
863 );
864 }
865
866 #[test]
867 fn paired_bus_shares_cell_nodes() {
868 let cs = ConstraintSystem::<F>::new();
869
870 let send_first = cs.col(PairedAirCols::S_SEND);
871 let recv_first = cs.col(PairedAirCols::S_RECV);
872
873 cs.assert_paired_bus_mutex(PairedAirCols::S_SEND, PairedAirCols::S_RECV);
874
875 let send_again = cs.col(PairedAirCols::S_SEND);
876 let recv_again = cs.col(PairedAirCols::S_RECV);
877
878 assert_eq!(send_first.id, send_again.id, "S_SEND must dedup");
879 assert_eq!(recv_first.id, recv_again.id, "S_RECV must dedup");
880 }
881
882 #[test]
883 fn chiplet_def_rejects_paired_spec_without_mutex() {
884 let bad = PairedAir { with_mutex: false };
885 assert_logup_bus_err(ChipletDef::<F>::from_air(&bad));
886 }
887
888 #[test]
889 fn chiplet_def_accepts_paired_spec_with_mutex() {
890 let good = PairedAir { with_mutex: true };
891 ChipletDef::<F>::from_air(&good).expect("paired AIR with mutex must snapshot");
892 }
893
894 #[test]
895 fn chiplet_def_rejects_paired_spec_without_boolean_roots() {
896 assert_logup_bus_err(ChipletDef::<F>::from_air(&PairedNoBoolAir));
897 }
898
899 #[test]
900 fn validator_rejects_recv_selector_without_send_selector() {
901 let spec = PermutationCheckSpec {
902 sources: vec![
903 (Source::Column(PairedAirCols::KEY), b"k_a" as ChallengeLabel),
904 (Source::RowIndexLeBytes(4), b"k_clk" as ChallengeLabel),
905 ],
906 selector: None,
907 recv_selector: Some(PairedAirCols::S_RECV),
908 kind: BusKind::Permutation,
909 clock_waiver: None,
910 };
911
912 let ast = ConstraintSystem::<F>::new().build();
913
914 assert_logup_bus_err(validate_paired_bus_mutex(
915 &[("asym_bus".into(), spec)],
916 &ast,
917 ));
918 }
919}