1use crate::constraint::{BoundaryConstraint, BoundaryTarget, ConstraintAst};
13use crate::expander::VirtualExpander;
14use crate::permutation::{PermutationCheckSpec, validate_fixed_selectors};
15use crate::{Air, FixedColumn, validate_fixed_columns};
16use alloc::boxed::Box;
17use alloc::string::String;
18use alloc::vec::Vec;
19use hekate_core::errors;
20use hekate_core::poly::PolyVariant;
21use hekate_core::trace::{ColumnTrace, ColumnType, Trace, TraceCompatibleField};
22use hekate_math::{Flat, HardwareField, PackableField, TowerField};
23
24#[derive(Clone)]
26pub struct ChipletDef<F: TowerField> {
27 name: String,
28 num_columns: usize,
29 constraint_ast: ConstraintAst<F>,
30 column_layout: Vec<ColumnType>,
31 virtual_column_layout: Vec<ColumnType>,
32 boundary_constraints: Vec<BoundaryConstraint<F>>,
33 fixed_columns: Vec<FixedColumn<F>>,
34 expander: Option<VirtualExpander>,
35 pub permutation_checks: Vec<(String, PermutationCheckSpec)>,
36}
37
38impl<F: TowerField> ChipletDef<F> {
39 pub fn from_air<P: Air<F> + Send + 'static>(p: &P) -> errors::Result<Self>
42 where
43 F: TraceCompatibleField + PackableField + HardwareField + 'static,
44 <F as PackableField>::Packed: Copy + Send + Sync,
45 {
46 let permutation_checks = p.permutation_checks();
47 for (bus_id, spec) in &permutation_checks {
48 spec.validate_clock_stitching(bus_id)?;
49 }
50
51 let constraint_ast = p.constraint_ast();
52 let boundary_constraints = p.boundary_constraints();
53 let fixed_columns = p.fixed_columns();
54
55 validate_fixed_selectors(&permutation_checks, &fixed_columns)?;
56 validate_chiplet_boundaries(&boundary_constraints, p.num_columns())?;
57 validate_fixed_columns(&fixed_columns, p.virtual_column_layout(), None)?;
58 validate_expander_coverage(p.virtual_expander(), p.column_layout())?;
59 validate_column_count(p.num_columns(), p.virtual_column_layout().len())?;
60
61 Ok(Self {
62 name: p.name(),
63 num_columns: p.num_columns(),
64 constraint_ast,
65 column_layout: p.column_layout().to_vec(),
66 virtual_column_layout: p.virtual_column_layout().to_vec(),
67 boundary_constraints,
68 fixed_columns,
69 expander: p.virtual_expander().cloned(),
70 permutation_checks,
71 })
72 }
73
74 pub fn ast(&self) -> &ConstraintAst<F> {
76 &self.constraint_ast
77 }
78
79 pub fn boundaries(&self) -> &[BoundaryConstraint<F>] {
80 &self.boundary_constraints
81 }
82
83 pub fn pins(&self) -> &[FixedColumn<F>] {
84 &self.fixed_columns
85 }
86
87 pub fn prefix_bus_ids(&mut self, prefix: &str, exempt: &[String]) {
90 for (bus_id, _) in &mut self.permutation_checks {
91 if !exempt.contains(bus_id) {
92 let mut prefixed = String::from(prefix);
93 prefixed.push_str("::");
94 prefixed.push_str(bus_id);
95
96 *bus_id = prefixed;
97 }
98 }
99 }
100
101 pub fn expand_variants<'a>(
104 &self,
105 trace: &'a ColumnTrace,
106 ) -> errors::Result<Vec<PolyVariant<'a, F>>>
107 where
108 F: TraceCompatibleField + 'static,
109 {
110 match &self.expander {
111 Some(e) => e.expand_variants(trace, 0),
112 None => trace.get_poly_variants::<F>(),
113 }
114 }
115
116 #[allow(clippy::too_many_arguments)]
119 pub fn from_wire(
120 name: String,
121 num_columns: usize,
122 constraint_ast: ConstraintAst<F>,
123 column_layout: Vec<ColumnType>,
124 virtual_column_layout: Vec<ColumnType>,
125 boundary_constraints: Vec<BoundaryConstraint<F>>,
126 fixed_columns: Vec<FixedColumn<F>>,
127 expander: Option<VirtualExpander>,
128 permutation_checks: Vec<(String, PermutationCheckSpec)>,
129 ) -> errors::Result<Self> {
130 for (bus_id, spec) in &permutation_checks {
131 spec.validate_clock_stitching(bus_id)?;
132 }
133
134 validate_fixed_selectors(&permutation_checks, &fixed_columns)?;
135 validate_chiplet_boundaries(&boundary_constraints, num_columns)?;
136
137 let virt_layout = match &expander {
138 Some(e) => e.virtual_layout(),
139 None => virtual_column_layout.as_slice(),
140 };
141
142 validate_fixed_columns(&fixed_columns, virt_layout, None)?;
143 validate_column_count(num_columns, virt_layout.len())?;
144
145 Ok(Self::from_parts(
146 name,
147 num_columns,
148 constraint_ast,
149 column_layout,
150 virtual_column_layout,
151 boundary_constraints,
152 fixed_columns,
153 expander,
154 permutation_checks,
155 ))
156 }
157
158 #[allow(clippy::too_many_arguments)]
161 pub(crate) fn from_parts(
162 name: String,
163 num_columns: usize,
164 constraint_ast: ConstraintAst<F>,
165 column_layout: Vec<ColumnType>,
166 virtual_column_layout: Vec<ColumnType>,
167 boundary_constraints: Vec<BoundaryConstraint<F>>,
168 fixed_columns: Vec<FixedColumn<F>>,
169 expander: Option<VirtualExpander>,
170 permutation_checks: Vec<(String, PermutationCheckSpec)>,
171 ) -> Self {
172 Self {
173 name,
174 num_columns,
175 constraint_ast,
176 column_layout,
177 virtual_column_layout,
178 boundary_constraints,
179 fixed_columns,
180 expander,
181 permutation_checks,
182 }
183 }
184}
185
186impl<F: TowerField> Air<F> for ChipletDef<F> {
187 fn name(&self) -> String {
188 self.name.clone()
189 }
190
191 fn num_columns(&self) -> usize {
192 self.num_columns
193 }
194
195 fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
196 self.boundary_constraints.clone()
197 }
198
199 fn column_layout(&self) -> &[ColumnType] {
200 &self.column_layout
201 }
202
203 fn virtual_column_layout(&self) -> &[ColumnType] {
204 match &self.expander {
205 Some(e) => e.virtual_layout(),
206 None => &self.virtual_column_layout,
207 }
208 }
209
210 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
211 self.permutation_checks.clone()
212 }
213
214 fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
215 self.fixed_columns.clone()
216 }
217
218 fn virtual_expander(&self) -> Option<&VirtualExpander> {
219 self.expander.as_ref()
220 }
221
222 fn parse_virtual_row(&self, bytes: &[u8], res: &mut Vec<Flat<F>>)
223 where
224 F: TraceCompatibleField,
225 {
226 if let Some(e) = &self.expander {
227 res.clear();
228
229 e.parse_row(bytes, res)
230 .expect("committed row byte length must match physical_row_bytes");
231 return;
232 }
233
234 res.clear();
235
236 let mut offset = 0;
237 for col_type in &self.column_layout {
238 let size = col_type.byte_size();
239 if offset + size <= bytes.len() {
240 res.push(col_type.parse_from_bytes(&bytes[offset..offset + size]));
241 offset += size;
242 }
243 }
244 }
245
246 fn constraint_ast(&self) -> ConstraintAst<F> {
247 self.constraint_ast.clone()
248 }
249}
250
251trait AirFactory<F: TowerField>: Send + Sync {
257 fn build(&self) -> errors::Result<ChipletDef<F>>;
258 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)>;
259 fn clone_box(&self) -> Box<dyn AirFactory<F>>;
260}
261
262impl<F, A> AirFactory<F> for A
263where
264 F: TowerField + TraceCompatibleField + PackableField + HardwareField + 'static,
265 <F as PackableField>::Packed: Copy + Send + Sync,
266 A: Air<F> + Clone + Send + Sync + 'static,
267{
268 fn build(&self) -> errors::Result<ChipletDef<F>> {
269 ChipletDef::from_air(self)
270 }
271
272 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
273 Air::permutation_checks(self)
274 }
275
276 fn clone_box(&self) -> Box<dyn AirFactory<F>> {
277 Box::new(self.clone())
278 }
279}
280
281impl<F: TowerField> Clone for Box<dyn AirFactory<F>> {
282 fn clone(&self) -> Self {
283 self.clone_box()
284 }
285}
286
287struct CompositeEntry<F: TraceCompatibleField> {
288 air: Box<dyn AirFactory<F>>,
289}
290
291impl<F: TraceCompatibleField> Clone for CompositeEntry<F> {
292 fn clone(&self) -> Self {
293 Self {
294 air: self.air.clone_box(),
295 }
296 }
297}
298
299pub struct CompositeChiplet<F: TraceCompatibleField> {
311 name: String,
312 chiplets: Vec<CompositeEntry<F>>,
313 external_bus_ids: Vec<String>,
314 external_buses: Vec<(String, PermutationCheckSpec)>,
315}
316
317impl<F: TraceCompatibleField> Clone for CompositeChiplet<F> {
318 fn clone(&self) -> Self {
319 Self {
320 name: self.name.clone(),
321 chiplets: self.chiplets.clone(),
322 external_bus_ids: self.external_bus_ids.clone(),
323 external_buses: self.external_buses.clone(),
324 }
325 }
326}
327
328impl<F: TraceCompatibleField> CompositeChiplet<F> {
329 pub fn builder(name: &str) -> CompositeChipletBuilder<F> {
332 CompositeChipletBuilder {
333 name: String::from(name),
334 chiplets: Vec::new(),
335 external_bus_ids: Vec::new(),
336 external_buses: Vec::new(),
337 }
338 }
339
340 pub fn flatten_defs(&self) -> errors::Result<Vec<ChipletDef<F>>> {
342 let mut out = Vec::with_capacity(self.chiplets.len());
343 for entry in &self.chiplets {
344 let mut def = entry.air.build()?;
345 def.prefix_bus_ids(&self.name, &self.external_bus_ids);
346
347 out.push(def);
348 }
349
350 Ok(out)
351 }
352
353 pub fn external_buses(&self) -> Vec<(String, PermutationCheckSpec)> {
358 self.external_buses.clone()
359 }
360
361 pub fn len(&self) -> usize {
363 self.chiplets.len()
364 }
365
366 pub fn is_empty(&self) -> bool {
368 self.chiplets.is_empty()
369 }
370
371 pub fn name(&self) -> &str {
373 &self.name
374 }
375}
376
377pub struct CompositeChipletBuilder<F: TraceCompatibleField> {
379 name: String,
380 chiplets: Vec<CompositeEntry<F>>,
381 external_bus_ids: Vec<String>,
382 external_buses: Vec<(String, PermutationCheckSpec)>,
383}
384
385impl<F: TraceCompatibleField> CompositeChipletBuilder<F> {
386 pub fn chiplet<A>(mut self, air: A) -> Self
388 where
389 A: Air<F> + Clone + Send + Sync + 'static,
390 F: TraceCompatibleField + PackableField + HardwareField + 'static,
391 <F as PackableField>::Packed: Copy + Send + Sync,
392 {
393 self.chiplets.push(CompositeEntry { air: Box::new(air) });
394
395 self
396 }
397
398 pub fn external_bus(mut self, bus_id: &str, spec: PermutationCheckSpec) -> Self {
400 self.external_bus_ids.push(String::from(bus_id));
401 self.external_buses.push((String::from(bus_id), spec));
402
403 self
404 }
405
406 pub fn build(self) -> errors::Result<CompositeChiplet<F>> {
412 for (bus_id, spec) in &self.external_buses {
413 spec.validate_clock_stitching(bus_id)?;
414 }
415
416 for entry in &self.chiplets {
417 let checks = entry.air.permutation_checks();
418 for i in 0..checks.len() {
419 for j in (i + 1)..checks.len() {
420 if checks[i].0 == checks[j].0 {
421 continue;
422 }
423
424 if let (Some(sel_i), Some(sel_j)) = (checks[i].1.selector, checks[j].1.selector)
425 && sel_i == sel_j
426 {
427 return Err(errors::Error::Protocol {
428 protocol: "composite_chiplet",
429 message: "different bus_ids share a selector column",
430 });
431 }
432 }
433 }
434 }
435
436 Ok(CompositeChiplet {
437 name: self.name,
438 chiplets: self.chiplets,
439 external_bus_ids: self.external_bus_ids,
440 external_buses: self.external_buses,
441 })
442 }
443}
444
445pub fn compose_chiplet_defs<F: TraceCompatibleField>(
454 composites: &[&CompositeChiplet<F>],
455) -> errors::Result<Vec<ChipletDef<F>>> {
456 for i in 0..composites.len() {
457 for j in (i + 1)..composites.len() {
458 if composites[i].name == composites[j].name {
459 return Err(errors::Error::Protocol {
460 protocol: "composite_chiplet",
461 message: "duplicate composite name in compose_chiplet_defs",
462 });
463 }
464 }
465 }
466
467 let mut defs = Vec::new();
468 for composite in composites {
469 defs.extend(composite.flatten_defs()?);
470 }
471
472 let endpoints = defs
473 .iter()
474 .flat_map(|d| d.permutation_checks.iter().map(|(id, s)| (id.as_str(), s)));
475
476 crate::permutation::validate_bus_set(endpoints)?;
477
478 Ok(defs)
479}
480
481pub fn compose_external_buses<F: TraceCompatibleField>(
483 composites: &[&CompositeChiplet<F>],
484) -> Vec<(String, PermutationCheckSpec)> {
485 let mut buses = Vec::new();
486 for composite in composites {
487 buses.extend(composite.external_buses());
488 }
489
490 buses
491}
492
493fn validate_chiplet_boundaries<F>(
497 boundaries: &[BoundaryConstraint<F>],
498 num_columns: usize,
499) -> errors::Result<()> {
500 for bc in boundaries {
501 if bc.col_idx >= num_columns {
502 return Err(errors::Error::Protocol {
503 protocol: "boundary",
504 message: "chiplet boundary col_idx out of range",
505 });
506 }
507
508 if matches!(bc.target, BoundaryTarget::PublicInput(_)) {
509 return Err(errors::Error::Protocol {
510 protocol: "boundary",
511 message: "chiplet boundaries must use BoundaryTarget::Constant",
512 });
513 }
514 }
515
516 Ok(())
517}
518
519fn validate_expander_coverage(
522 expander: Option<&VirtualExpander>,
523 layout: &[ColumnType],
524) -> errors::Result<()> {
525 let covered = match expander {
526 Some(e) => e.num_physical_columns(),
527 None => layout.len(),
528 };
529
530 if covered != layout.len() {
531 return Err(errors::Error::Protocol {
532 protocol: "chiplet",
533 message: "virtual_expander does not tile column_layout",
534 });
535 }
536
537 Ok(())
538}
539
540fn validate_column_count(num_columns: usize, virtual_columns: usize) -> errors::Result<()> {
541 if num_columns != virtual_columns {
542 return Err(errors::Error::Protocol {
543 protocol: "chiplet",
544 message: "num_columns does not match the virtual column layout",
545 });
546 }
547
548 Ok(())
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554 use crate::constraint::builder::ConstraintSystem;
555 use crate::define_columns;
556 use crate::permutation::{BusKind, ChallengeLabel, PermutationCheckSpec, Source};
557 use crate::{ConstraintAst, FixedShape};
558 use alloc::string::String;
559 use alloc::vec;
560 use hekate_core::trace::ColumnType;
561 use hekate_math::Block128;
562
563 type F = Block128;
564
565 define_columns! {
566 PairedAirCols {
567 KEY: B32,
568 S_SEND: Bit,
569 S_RECV: Bit,
570 }
571 }
572
573 #[derive(Clone)]
574 struct ExpanderAir {
575 expander: VirtualExpander,
576 }
577
578 impl Air<F> for ExpanderAir {
579 fn num_columns(&self) -> usize {
580 self.expander.virtual_layout().len()
581 }
582
583 fn column_layout(&self) -> &[ColumnType] {
584 &[ColumnType::B32, ColumnType::Bit]
585 }
586
587 fn virtual_expander(&self) -> Option<&VirtualExpander> {
588 Some(&self.expander)
589 }
590
591 fn constraint_ast(&self) -> ConstraintAst<F> {
592 ConstraintSystem::<F>::new().build()
593 }
594 }
595
596 #[derive(Clone)]
597 struct OneBusAir {
598 spec: PermutationCheckSpec,
599 }
600
601 impl Air<F> for OneBusAir {
602 fn num_columns(&self) -> usize {
603 2
604 }
605
606 fn column_layout(&self) -> &[ColumnType] {
607 &[ColumnType::B32, ColumnType::Bit]
608 }
609
610 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
611 vec![("test_bus".into(), self.spec.clone())]
612 }
613
614 fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
615 liveness_pin(1)
616 }
617
618 fn constraint_ast(&self) -> ConstraintAst<F> {
619 ConstraintSystem::<F>::new().build()
620 }
621 }
622
623 #[derive(Clone)]
624 struct PairedAir {
625 pinned: bool,
626 }
627
628 impl Air<F> for PairedAir {
629 fn num_columns(&self) -> usize {
630 PairedAirCols::NUM_COLUMNS
631 }
632
633 fn column_layout(&self) -> &[ColumnType] {
634 static LAYOUT: std::sync::OnceLock<Vec<ColumnType>> = std::sync::OnceLock::new();
635 LAYOUT.get_or_init(PairedAirCols::build_layout)
636 }
637
638 fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
639 let sources = vec![
640 (Source::Column(PairedAirCols::KEY), b"k_a" as ChallengeLabel),
641 (Source::RowIndexLeBytes(4), b"k_clk" as ChallengeLabel),
642 ];
643
644 vec![(
645 "paired_test_bus".into(),
646 PermutationCheckSpec::new_paired(
647 sources,
648 PairedAirCols::S_SEND,
649 PairedAirCols::S_RECV,
650 BusKind::Permutation,
651 ),
652 )]
653 }
654
655 fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
656 match self.pinned {
657 true => vec![
658 FixedColumn::sparse(PairedAirCols::S_SEND, vec![(0, F::ONE)]),
659 FixedColumn::sparse(PairedAirCols::S_RECV, vec![(1, F::ONE)]),
660 ],
661 false => Vec::new(),
662 }
663 }
664
665 fn constraint_ast(&self) -> ConstraintAst<F> {
666 ConstraintSystem::<F>::new().build()
667 }
668 }
669
670 fn key_only() -> Vec<(Source, ChallengeLabel)> {
671 vec![(Source::Column(0), b"k_a")]
672 }
673
674 fn key_with_clock() -> Vec<(Source, ChallengeLabel)> {
675 vec![
676 (Source::Column(0), b"k_a"),
677 (Source::RowIndexLeBytes(4), b"k_clk"),
678 ]
679 }
680
681 fn snapshot(spec: PermutationCheckSpec) -> errors::Result<ChipletDef<F>> {
682 ChipletDef::from_air(&OneBusAir { spec })
683 }
684
685 fn assert_logup_bus_err<T>(res: errors::Result<T>) {
686 match res {
687 Err(errors::Error::Protocol { protocol, .. }) => {
688 assert_eq!(protocol, "logup_bus");
689 }
690 Ok(_) => panic!("expected Err(Protocol {{ protocol: \"logup_bus\", .. }})"),
691 Err(other) => panic!("expected Err(Protocol), got {:?}", other),
692 }
693 }
694
695 fn liveness_pin(col: usize) -> Vec<FixedColumn<F>> {
696 vec![FixedColumn::sparse(col, vec![(0, F::ONE)])]
697 }
698
699 #[test]
700 fn def_rejects_permutation_without_clock() {
701 let spec = PermutationCheckSpec::new(key_only(), Some(1));
702 assert_logup_bus_err(snapshot(spec));
703 }
704
705 #[test]
706 fn def_rejects_permutation_with_empty_waiver() {
707 let spec = PermutationCheckSpec::new(key_only(), Some(1)).with_clock_waiver("");
708 assert_logup_bus_err(snapshot(spec));
709 }
710
711 #[test]
712 fn def_rejects_permutation_with_clock_and_waiver() {
713 let spec =
714 PermutationCheckSpec::new(key_with_clock(), Some(1)).with_clock_waiver("redundant");
715 assert_logup_bus_err(snapshot(spec));
716 }
717
718 #[test]
719 fn def_rejects_lookup_with_waiver() {
720 let spec = PermutationCheckSpec::new_lookup(key_only(), Some(1)).with_clock_waiver("nope");
721 assert_logup_bus_err(snapshot(spec));
722 }
723
724 #[test]
725 fn def_accepts_permutation_with_row_index() {
726 let spec = PermutationCheckSpec::new(key_with_clock(), Some(1));
727 snapshot(spec).expect("permutation bus with row-index source must accept");
728 }
729
730 #[test]
731 fn def_accepts_permutation_with_clock_waiver() {
732 let spec = PermutationCheckSpec::new(key_only(), Some(1))
733 .with_clock_waiver("see foo.rs:42: structurally unique by AIR body");
734 snapshot(spec).expect("permutation bus with non-empty clock_waiver must accept");
735 }
736
737 #[test]
738 fn def_accepts_lookup_without_clock() {
739 let spec = PermutationCheckSpec::new_lookup(key_only(), Some(1));
740 snapshot(spec).expect("lookup bus without clock must accept");
741 }
742
743 #[test]
744 fn def_rejects_permutation_with_too_short_waiver() {
745 let spec = PermutationCheckSpec::new(key_only(), Some(1)).with_clock_waiver("see x.rs");
746 assert_logup_bus_err(snapshot(spec));
747 }
748
749 #[test]
750 fn def_rejects_permutation_with_missing_see_citation() {
751 let spec = PermutationCheckSpec::new(key_only(), Some(1))
752 .with_clock_waiver("structurally unique by AIR body but no file citation prefix here");
753 assert_logup_bus_err(snapshot(spec));
754 }
755
756 #[test]
757 fn chiplet_def_rejects_paired_spec_with_witness_selectors() {
758 assert_logup_bus_err(ChipletDef::<F>::from_air(&PairedAir { pinned: false }));
759 }
760
761 #[test]
762 fn chiplet_def_accepts_paired_spec_with_fixed_selectors() {
763 ChipletDef::<F>::from_air(&PairedAir { pinned: true })
764 .expect("paired AIR with fixed selectors must snapshot");
765 }
766
767 #[test]
768 fn chiplet_def_requires_expander_to_tile_the_layout() {
769 let tiled = ExpanderAir {
770 expander: VirtualExpander::new()
771 .expand_bits(1, ColumnType::B32)
772 .control_bits(1)
773 .build()
774 .unwrap(),
775 };
776
777 ChipletDef::<F>::from_air(&tiled).expect("tiling expander must snapshot");
778
779 let short = ExpanderAir {
780 expander: VirtualExpander::new()
781 .expand_bits(1, ColumnType::B32)
782 .build()
783 .unwrap(),
784 };
785
786 assert!(ChipletDef::<F>::from_air(&short).is_err());
787 }
788
789 #[test]
790 fn validator_rejects_recv_selector_without_send_selector() {
791 let spec = PermutationCheckSpec {
792 sources: vec![
793 (Source::Column(PairedAirCols::KEY), b"k_a" as ChallengeLabel),
794 (Source::RowIndexLeBytes(4), b"k_clk" as ChallengeLabel),
795 ],
796 selector: None,
797 recv_selector: Some(PairedAirCols::S_RECV),
798 kind: BusKind::Permutation,
799 clock_waiver: None,
800 };
801
802 assert_logup_bus_err(validate_fixed_selectors::<F>(
803 &[("asym_bus".into(), spec)],
804 &liveness_pin(PairedAirCols::S_RECV),
805 ));
806 }
807
808 #[test]
809 fn def_rejects_witness_selector() {
810 let spec = PermutationCheckSpec::new(key_with_clock(), Some(0));
811 assert_logup_bus_err(snapshot(spec));
812 }
813
814 #[test]
815 fn def_accepts_absent_selector() {
816 let spec = PermutationCheckSpec::new(key_with_clock(), None);
817 snapshot(spec).expect("selector-free bus must accept");
818 }
819
820 #[test]
821 fn validator_rejects_selector_pinned_to_substituted_shape() {
822 let spec = PermutationCheckSpec::new(key_with_clock(), Some(1));
823
824 for shape in [
825 FixedShape::FirstRow,
826 FixedShape::LastRow,
827 FixedShape::Custom(vec![true, false]),
828 ] {
829 assert_logup_bus_err(validate_fixed_selectors(
830 &[("svc".into(), spec.clone())],
831 &[FixedColumn::<F> { col_idx: 1, shape }],
832 ));
833 }
834 }
835
836 #[test]
837 fn validator_accepts_selector_pinned_to_overlay_shape() {
838 let spec = PermutationCheckSpec::new(key_with_clock(), Some(1));
839
840 validate_fixed_selectors(&[("svc".into(), spec)], &liveness_pin(1))
841 .expect("overlay-pinned selector must accept");
842 }
843}