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