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