Skip to main content

hekate_program/
chiplet.rs

1// SPDX-FileCopyrightText: 2026 Andrei Kochergin <andrei@oumuamua.dev>
2// SPDX-FileCopyrightText: 2026 Oumuamua Labs <info@oumuamua.dev>
3// SPDX-License-Identifier: AGPL-3.0-only
4
5//! Independent AIR Chiplet definitions.
6//!
7//! A `ChipletDef` snapshots a chiplet's full AIR
8//! (constraints, layout, bus specs) into an owned struct.
9//! The prover runs an independent ZeroCheck per chiplet.
10//! The bus (GPA) reconnects chiplets to the main trace.
11
12use 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/// Pre-computed chiplet AIR definition.
25#[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    /// Snapshot a chiplet's full AIR definition.
40    /// Call once at setup; the source chiplet can be dropped after.
41    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    /// Borrowed views of what the `Air` getters clone.
75    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    /// Prefixes internal bus_ids with a namespace.
88    /// Bus_ids listed in `exempt` are left unchanged.
89    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    /// Expand physical ColumnTrace into virtual PolyVariants.
102    /// Uses embedded expander if present, else 1:1 mapping.
103    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    /// Reconstruct from deserialized wire data.
117    /// Validates every embedded `PermutationCheckSpec`.
118    #[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    /// Main-table construction path; callers own
159    /// validation (chiplet boundary rules do not apply).
160    #[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
251// =================================================================
252// Composite Chiplet Composition
253// =================================================================
254
255/// Factory trait for deterministic ChipletDef construction.
256trait 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
299/// Build-time composition of peer
300/// chiplets into a single unit.
301///
302/// Flattens into standard `ChipletDef` entries
303/// for the prover, no protocol-level awareness
304/// of hierarchy. Internal buses are namespaced
305/// with `"{name}::"` to prevent cross-composite
306/// collisions. External buses pass through
307/// unchanged.
308///
309/// All chiplets within a composite are peers, no mandatory root.
310pub 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    /// Start building a composite
330    /// with the given namespace.
331    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    /// Produce fresh ChipletDefs for `Program::chiplet_defs()`.
341    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    /// External buses for `Program::permutation_checks()`.
354    ///
355    /// These are main-trace-side specs, column indices
356    /// reference the main trace, not any chiplet trace.
357    pub fn external_buses(&self) -> Vec<(String, PermutationCheckSpec)> {
358        self.external_buses.clone()
359    }
360
361    /// Number of flattened chiplets in this composite.
362    pub fn len(&self) -> usize {
363        self.chiplets.len()
364    }
365
366    /// Returns true if this composite contains no chiplets.
367    pub fn is_empty(&self) -> bool {
368        self.chiplets.is_empty()
369    }
370
371    /// The composite's namespace.
372    pub fn name(&self) -> &str {
373        &self.name
374    }
375}
376
377/// Builder for `CompositeChiplet`.
378pub 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    /// Add a sub-chiplet.
387    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    /// Declare an external bus (connects to the main trace).
399    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    /// Finalize the composite.
407    ///
408    /// Validates selector orthogonality:
409    /// two specs on different bus_ids must not share a selector column
410    /// index. Same bus_id is exempt for dual-spec intra-table check.
411    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
445// =================================================================
446// Multi-Composite Helpers
447// =================================================================
448
449/// Flatten multiple composites into a single chiplet def list.
450///
451/// Validates that no two composites share the same name
452/// (would cause bus namespace collisions).
453pub 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
481/// Collect external buses from multiple composites.
482pub 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
493/// Chiplets carry no `public_inputs`; a `PublicInput`
494/// boundary target is unsatisfiable; reject it at
495/// snapshot time. Also rejects out-of-range `col_idx`.
496fn 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
519/// Physical columns outside every expansion entry enter
520/// no master fold; nothing binds their committed cells.
521fn 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}