Skip to main content

hekate_program/
circuit.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//! Single-pass program authoring.
6//!
7//! A `Circuit` records declarations in order and compiles
8//! them into a `CircuitProgram`. Offsets, bus endpoints,
9//! kernel hints, and the expander are engine-derived;
10//! public inputs exist only through `publish`.
11
12use crate::chiplet::ChipletDef;
13use crate::constraint::builder::ConstraintSystem;
14use crate::constraint::{BoundaryConstraint, BoundaryTarget, ConstraintAst};
15use crate::expander::VirtualExpander;
16use crate::permutation::{
17    PermutationCheckSpec, Service, Source, validate_bus_set, validate_fixed_selectors,
18};
19use crate::{Air, FixedColumn, FixedShape, InlineKernelHint, Program, validate_fixed_columns};
20use alloc::string::String;
21use alloc::vec::Vec;
22use hekate_core::errors;
23use hekate_core::trace::ColumnType;
24use hekate_math::{HardwareField, TowerField};
25
26/// Virtual column handle issued by `Circuit`.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct Col(usize);
29
30impl Col {
31    #[inline(always)]
32    pub fn index(self) -> usize {
33        self.0
34    }
35}
36
37/// Contiguous run of virtual columns.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct ColRange {
40    start: usize,
41    count: usize,
42}
43
44impl ColRange {
45    pub fn at(self, i: usize) -> Col {
46        assert!(i < self.count, "ColRange::at out of range");
47        Col(self.start + i)
48    }
49
50    #[inline(always)]
51    pub fn start(self) -> usize {
52        self.start
53    }
54
55    #[inline(always)]
56    pub fn len(self) -> usize {
57        self.count
58    }
59
60    #[inline(always)]
61    pub fn is_empty(self) -> bool {
62        self.count == 0
63    }
64
65    pub fn iter(self) -> impl Iterator<Item = Col> {
66        (self.start..self.start + self.count).map(Col)
67    }
68}
69
70/// Handle to a bit-expanded physical column group.
71#[derive(Clone, Copy, Debug)]
72pub struct Packed {
73    phys_start: usize,
74    count: usize,
75    bits_per: usize,
76    bits: ColRange,
77}
78
79impl Packed {
80    /// Bit columns of the group's `i`-th physical column.
81    ///
82    /// # Panics
83    /// `i >= count`.
84    pub fn bits(&self, i: usize) -> ColRange {
85        assert!(i < self.count, "Packed::bits out of range");
86        ColRange {
87            start: self.bits.start + i * self.bits_per,
88            count: self.bits_per,
89        }
90    }
91}
92
93/// Handle to an inline-mounted chiplet.
94#[derive(Clone, Copy, Debug)]
95pub struct Mounted {
96    offset: usize,
97    num_columns: usize,
98}
99
100impl Mounted {
101    /// # Panics
102    /// `local >= num_columns()`.
103    pub fn col(&self, local: usize) -> Col {
104        assert!(local < self.num_columns, "Mounted::col out of range");
105        Col(self.offset + local)
106    }
107
108    #[inline(always)]
109    pub fn offset(&self) -> usize {
110        self.offset
111    }
112
113    #[inline(always)]
114    pub fn num_columns(&self) -> usize {
115        self.num_columns
116    }
117}
118
119/// Recorder for one program: main-table declarations
120/// plus mounted and attached chiplets.
121pub struct Circuit<F: TowerField> {
122    name: String,
123    num_rows: usize,
124    cs: ConstraintSystem<F>,
125    physical: Vec<ColumnType>,
126    expander: VirtualExpander,
127    pending_run: Option<(ColumnType, usize)>,
128    num_virtual: usize,
129    fixed: Vec<FixedColumn<F>>,
130    boundaries: Vec<BoundaryConstraint<F>>,
131    buses: Vec<(String, PermutationCheckSpec)>,
132    num_public_inputs: usize,
133    inline_chiplets: Vec<ChipletDef<F>>,
134    inline_hints: Vec<InlineKernelHint>,
135    chiplet_defs: Vec<ChipletDef<F>>,
136}
137
138impl<F: TowerField + HardwareField> Circuit<F> {
139    /// # Errors
140    /// `num_rows` not a power of two.
141    pub fn new(name: &str, num_rows: usize) -> errors::Result<Self> {
142        if !num_rows.is_power_of_two() {
143            return Err(errors::Error::Protocol {
144                protocol: "circuit",
145                message: "num_rows must be a power of two",
146            });
147        }
148
149        Ok(Self {
150            name: String::from(name),
151            num_rows,
152            cs: ConstraintSystem::new(),
153            physical: Vec::new(),
154            expander: VirtualExpander::new(),
155            pending_run: None,
156            num_virtual: 0,
157            fixed: Vec::new(),
158            boundaries: Vec::new(),
159            buses: Vec::new(),
160            num_public_inputs: 0,
161            inline_chiplets: Vec::new(),
162            inline_hints: Vec::new(),
163            chiplet_defs: Vec::new(),
164        })
165    }
166
167    #[inline(always)]
168    pub fn num_rows(&self) -> usize {
169        self.num_rows
170    }
171
172    /// Constraint DSL over the recorded columns.
173    /// Column handles convert via `Col::index`.
174    pub fn cs(&self) -> &ConstraintSystem<F> {
175        &self.cs
176    }
177
178    pub fn column(&mut self, ty: ColumnType) -> Col {
179        self.columns(1, ty).at(0)
180    }
181
182    /// Records a `define_columns!` layout 1:1, physical to
183    /// virtual. Index the range with the schema's own constants.
184    pub fn schema(&mut self, layout: &[ColumnType]) -> ColRange {
185        let start = self.num_virtual;
186
187        for &ty in layout {
188            self.column(ty);
189        }
190
191        ColRange {
192            start,
193            count: layout.len(),
194        }
195    }
196
197    pub fn columns(&mut self, count: usize, ty: ColumnType) -> ColRange {
198        let start = self.num_virtual;
199
200        self.physical.extend(core::iter::repeat_n(ty, count));
201        self.num_virtual += count;
202
203        match self.pending_run.take() {
204            Some((run_ty, run_count)) if run_ty == ty => {
205                self.pending_run = Some((ty, run_count + count));
206            }
207            prior => {
208                self.flush_run(prior);
209
210                self.pending_run = Some((ty, count));
211            }
212        }
213
214        ColRange { start, count }
215    }
216
217    /// `count` physical `storage` columns exposed
218    /// as `count × bit-width` virtual `Bit` columns.
219    pub fn expand_bits(&mut self, count: usize, storage: ColumnType) -> Packed {
220        self.flush_pending();
221
222        let phys_start = self.physical.len();
223        let bits_per = storage.byte_size() * 8;
224        let bits = ColRange {
225            start: self.num_virtual,
226            count: count * bits_per,
227        };
228
229        self.physical.extend(core::iter::repeat_n(storage, count));
230        self.num_virtual += bits.count;
231
232        let expander = core::mem::take(&mut self.expander);
233
234        self.expander = expander.expand_bits(count, storage);
235
236        Packed {
237            phys_start,
238            count,
239            bits_per,
240            bits,
241        }
242    }
243
244    /// Whole-column virtual view of
245    /// a bit-expanded physical group.
246    pub fn reuse_pass_through(&mut self, packed: &Packed) -> ColRange {
247        self.flush_pending();
248
249        let view = ColRange {
250            start: self.num_virtual,
251            count: packed.count,
252        };
253
254        self.num_virtual += packed.count;
255
256        let expander = core::mem::take(&mut self.expander);
257
258        self.expander = expander.reuse_pass_through(packed.phys_start, packed.count);
259
260        view
261    }
262
263    /// Pins `col` to `shape`; the verifier evaluates
264    /// the shape's MLE for itself at `r_final`.
265    pub fn fix(&mut self, col: Col, shape: FixedShape<F>) {
266        self.fixed.push(FixedColumn {
267            col_idx: col.index(),
268            shape,
269        });
270    }
271
272    /// Constant-target pin;
273    /// public inputs enter only through `publish`.
274    pub fn boundary(&mut self, col: Col, row: usize, value: F) {
275        self.boundaries.push(BoundaryConstraint {
276            col_idx: col.index(),
277            row_idx: row,
278            target: BoundaryTarget::Constant(value),
279        });
280    }
281
282    /// Requester endpoint of `service` on this table.
283    pub fn call(&mut self, service: &Service, values: &[Col], selector: Col) -> errors::Result<()> {
284        let cols: Vec<usize> = values.iter().map(|c| c.index()).collect();
285        let spec = service.request(&cols, selector.index())?;
286
287        self.buses.push((String::from(service.bus_id), spec));
288
289        Ok(())
290    }
291
292    /// Raw bus endpoint for shapes `Service` does
293    /// not model (positional, paired, waivered).
294    pub fn bus(&mut self, bus_id: &str, spec: PermutationCheckSpec) {
295        self.buses.push((String::from(bus_id), spec));
296    }
297
298    /// Mounts a chiplet inline: its artifacts and bus endpoints
299    /// merge into this table at engine-computed offsets.
300    pub fn mount(&mut self, def: ChipletDef<F>) -> Mounted {
301        self.mount_inner(def, true)
302    }
303
304    /// Mounts a chiplet used as the program's own
305    /// trace, whose declared bus has no partner here.
306    pub fn mount_unlinked(&mut self, def: ChipletDef<F>) -> Mounted {
307        self.mount_inner(def, false)
308    }
309
310    /// Attaches a chiplet as an independent table
311    /// with its own trace, commitment, and ZeroCheck.
312    pub fn attach(&mut self, def: ChipletDef<F>) {
313        self.chiplet_defs.push(def);
314    }
315
316    /// Pins `col` at `row` to a fresh public input slot,
317    /// returned. Whether the AIR determines the pinned
318    /// cell is the audited circuit's burden, not checked.
319    pub fn publish(&mut self, col: Col, row: usize) -> usize {
320        let slot = self.next_slot();
321
322        self.boundaries.push(BoundaryConstraint::with_public_input(
323            col.index(),
324            row,
325            slot,
326        ));
327
328        slot
329    }
330
331    /// Derives the program artifacts, running every
332    /// structural validator on the recording.
333    pub fn compile(mut self) -> errors::Result<CircuitProgram<F>> {
334        self.flush_pending();
335
336        let expander = self.expander.build()?;
337        let width = self.num_virtual;
338        let num_vars = self.num_rows.trailing_zeros() as usize;
339
340        if expander.num_virtual_columns() != width {
341            return Err(errors::Error::Protocol {
342                protocol: "circuit",
343                message: "expander virtual width diverged from declared columns",
344            });
345        }
346
347        let one_to_one = expander.num_virtual_columns() == expander.num_physical_columns();
348        let virtual_layout = expander.virtual_layout().to_vec();
349
350        let ast = self.cs.build();
351
352        for (bus_id, spec) in &self.buses {
353            spec.validate_clock_stitching(bus_id)?;
354
355            for (source, _) in &spec.sources {
356                validate_source_range(source, width)?;
357            }
358        }
359
360        validate_fixed_selectors(&self.buses, &self.fixed)?;
361
362        for bc in &self.boundaries {
363            if bc.col_idx >= width || bc.row_idx >= self.num_rows {
364                return Err(errors::Error::Protocol {
365                    protocol: "circuit",
366                    message: "boundary constraint outside the trace",
367                });
368            }
369        }
370
371        validate_fixed_columns(&self.fixed, &virtual_layout, Some(num_vars))?;
372
373        for bc in &self.boundaries {
374            if let BoundaryTarget::PublicInput(slot) = bc.target
375                && slot >= self.num_public_inputs
376            {
377                return Err(errors::Error::Protocol {
378                    protocol: "circuit",
379                    message: "boundary public input slot out of range",
380                });
381            }
382        }
383
384        let chiplet_specs: Vec<Vec<(String, PermutationCheckSpec)>> = self
385            .chiplet_defs
386            .iter()
387            .map(|cd| cd.permutation_checks())
388            .collect();
389
390        let endpoints = self
391            .buses
392            .iter()
393            .chain(chiplet_specs.iter().flatten())
394            .map(|(id, s)| (id.as_str(), s));
395
396        validate_bus_set(endpoints)?;
397
398        let table = ChipletDef::from_parts(
399            self.name,
400            width,
401            ast,
402            self.physical,
403            virtual_layout,
404            self.boundaries,
405            self.fixed,
406            (!one_to_one).then_some(expander),
407            self.buses,
408        );
409
410        Ok(CircuitProgram {
411            table,
412            num_public_inputs: self.num_public_inputs,
413            inline_chiplets: self.inline_chiplets,
414            inline_kernels: self.inline_hints,
415            chiplet_defs: self.chiplet_defs,
416        })
417    }
418
419    fn mount_inner(&mut self, def: ChipletDef<F>, link: bool) -> Mounted {
420        self.flush_pending();
421
422        let chiplet_idx = self.inline_chiplets.len();
423        let offset = self.num_virtual;
424        let num_columns = Air::<F>::num_columns(&def);
425
426        let mut ast = def.constraint_ast();
427        ast.arena.shift_cells(offset);
428
429        self.inline_hints.push(InlineKernelHint {
430            chiplet_idx,
431            root_offset: self.cs.num_roots(),
432            column_offset: offset,
433        });
434
435        self.cs.merge_ast(ast);
436
437        for mut fc in Air::<F>::fixed_columns(&def) {
438            fc.col_idx += offset;
439            self.fixed.push(fc);
440        }
441
442        for mut bc in def.boundary_constraints() {
443            bc.col_idx += offset;
444            self.boundaries.push(bc);
445        }
446
447        match def.virtual_expander() {
448            Some(e) => {
449                let expander = core::mem::take(&mut self.expander);
450
451                self.expander = expander.append(e);
452                self.physical.extend_from_slice(def.column_layout());
453                self.num_virtual += num_columns;
454            }
455            None => {
456                self.schema(def.column_layout());
457            }
458        }
459
460        if link {
461            for (bus_id, mut spec) in def.permutation_checks() {
462                spec.shift_column_indices(offset);
463                self.buses.push((bus_id, spec));
464            }
465        }
466
467        self.inline_chiplets.push(def);
468
469        Mounted {
470            offset,
471            num_columns,
472        }
473    }
474
475    fn next_slot(&mut self) -> usize {
476        let slot = self.num_public_inputs;
477        self.num_public_inputs += 1;
478
479        slot
480    }
481
482    fn flush_pending(&mut self) {
483        let prior = self.pending_run.take();
484        self.flush_run(prior);
485    }
486
487    fn flush_run(&mut self, run: Option<(ColumnType, usize)>) {
488        let Some((ty, count)) = run else {
489            return;
490        };
491
492        let expander = core::mem::take(&mut self.expander);
493        self.expander = match ty {
494            ColumnType::Bit => expander.control_bits(count),
495            _ => expander.pass_through(count, ty),
496        };
497    }
498}
499
500/// A `Circuit`'s compiled program.
501#[derive(Clone)]
502pub struct CircuitProgram<F: TowerField> {
503    table: ChipletDef<F>,
504    num_public_inputs: usize,
505    inline_chiplets: Vec<ChipletDef<F>>,
506    inline_kernels: Vec<InlineKernelHint>,
507    chiplet_defs: Vec<ChipletDef<F>>,
508}
509
510impl<F: TowerField> Air<F> for CircuitProgram<F> {
511    fn name(&self) -> String {
512        self.table.name()
513    }
514
515    fn num_columns(&self) -> usize {
516        self.table.num_columns()
517    }
518
519    fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
520        self.table.boundary_constraints()
521    }
522
523    fn column_layout(&self) -> &[ColumnType] {
524        self.table.column_layout()
525    }
526
527    fn virtual_column_layout(&self) -> &[ColumnType] {
528        self.table.virtual_column_layout()
529    }
530
531    fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
532        self.table.permutation_checks()
533    }
534
535    fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
536        self.table.fixed_columns()
537    }
538
539    fn virtual_expander(&self) -> Option<&VirtualExpander> {
540        self.table.virtual_expander()
541    }
542
543    fn constraint_ast(&self) -> ConstraintAst<F> {
544        self.table.constraint_ast()
545    }
546
547    fn inline_chiplets(&self) -> errors::Result<Vec<ChipletDef<F>>> {
548        Ok(self.inline_chiplets.clone())
549    }
550
551    fn inline_chiplet_kernels(&self) -> Vec<InlineKernelHint> {
552        self.inline_kernels.clone()
553    }
554}
555
556impl<F: TowerField> Program<F> for CircuitProgram<F> {
557    fn num_public_inputs(&self) -> usize {
558        self.num_public_inputs
559    }
560
561    fn chiplet_defs(&self) -> errors::Result<Vec<ChipletDef<F>>> {
562        Ok(self.chiplet_defs.clone())
563    }
564}
565
566fn validate_source_range(source: &Source, width: usize) -> errors::Result<()> {
567    let out_of_range = match source {
568        Source::Column(idx) | Source::PhaseColumn(idx) => *idx >= width,
569        Source::Columns(indices) => indices.iter().any(|&idx| idx >= width),
570        Source::RowIndexLeBytes(_) | Source::RowIndexByte(_) | Source::Const(_) => false,
571    };
572
573    if out_of_range {
574        return Err(errors::Error::Protocol {
575            protocol: "circuit",
576            message: "bus source column out of range",
577        });
578    }
579
580    Ok(())
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use crate::permutation::{REQUEST_IDX_LABEL, Source};
587    use alloc::string::ToString;
588    use alloc::vec;
589    use hekate_math::Block128;
590
591    type F = Block128;
592
593    #[derive(Clone)]
594    struct MiniChiplet;
595
596    impl Air<F> for MiniChiplet {
597        fn name(&self) -> String {
598            "MiniChiplet".to_string()
599        }
600
601        fn num_columns(&self) -> usize {
602            2
603        }
604
605        fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
606            vec![BoundaryConstraint::with_constant(0, 0, F::ZERO)]
607        }
608
609        fn column_layout(&self) -> &[ColumnType] {
610            &[ColumnType::B32, ColumnType::Bit]
611        }
612
613        fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
614            vec![(
615                "mini_bus".to_string(),
616                PermutationCheckSpec::new(
617                    vec![
618                        (Source::Column(0), b"k_v"),
619                        (Source::RowIndexLeBytes(4), REQUEST_IDX_LABEL),
620                    ],
621                    Some(1),
622                ),
623            )]
624        }
625
626        fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
627            vec![FixedColumn::prefix(1, 1)]
628        }
629
630        fn constraint_ast(&self) -> ConstraintAst<F> {
631            let cs = ConstraintSystem::<F>::new();
632            cs.assert_boolean(cs.col(1));
633
634            cs.build()
635        }
636    }
637
638    fn mini_def() -> ChipletDef<F> {
639        ChipletDef::from_air(&MiniChiplet).unwrap()
640    }
641
642    fn clocked_spec(key_col: usize, selector: usize) -> PermutationCheckSpec {
643        PermutationCheckSpec::new(
644            vec![
645                (Source::Column(key_col), b"k_v"),
646                (Source::RowIndexLeBytes(4), REQUEST_IDX_LABEL),
647            ],
648            Some(selector),
649        )
650    }
651
652    #[test]
653    fn mount_matches_manual_shift() {
654        let mut cx = Circuit::<F>::new("mini_host", 16).unwrap();
655        let host_key = cx.column(ColumnType::B32);
656
657        let cs = cx.cs();
658        cs.constrain(cs.col(host_key.index()) * cs.col(host_key.index()));
659
660        let mounted = cx.mount(mini_def());
661
662        assert_eq!(mounted.offset(), 1);
663
664        let program = cx.compile().unwrap();
665
666        assert_eq!(
667            program.column_layout(),
668            &[ColumnType::B32, ColumnType::B32, ColumnType::Bit]
669        );
670
671        let hints = program.inline_chiplet_kernels();
672
673        assert_eq!(hints.len(), 1);
674        assert_eq!(hints[0].chiplet_idx, 0);
675        assert_eq!(hints[0].root_offset, 1);
676        assert_eq!(hints[0].column_offset, 1);
677
678        let mut manual = {
679            let cs = ConstraintSystem::<F>::new();
680            cs.constrain(cs.col(0) * cs.col(0));
681
682            cs.build()
683        };
684
685        let mut chiplet_ast = Air::<F>::constraint_ast(&MiniChiplet);
686        chiplet_ast.arena.shift_cells(1);
687
688        manual.merge(chiplet_ast);
689
690        assert_eq!(
691            program.constraint_ast().to_constraints(),
692            manual.to_constraints()
693        );
694
695        let specs = program.permutation_checks();
696
697        assert_eq!(specs.len(), 1);
698        assert_eq!(specs[0].1.selector, Some(2));
699        assert_eq!(specs[0].1.sources[0].0, Source::Column(1));
700
701        let pins = program.fixed_columns();
702
703        assert_eq!(pins.len(), 1);
704        assert_eq!(pins[0].col_idx, 2);
705
706        let boundaries = program.boundary_constraints();
707
708        assert_eq!(boundaries.len(), 1);
709        assert_eq!(boundaries[0].col_idx, 1);
710    }
711
712    #[test]
713    fn mount_unlinked_keeps_columns_and_drops_endpoint() {
714        let mut cx = Circuit::<F>::new("isolated_host", 16).unwrap();
715        let mounted = cx.mount_unlinked(mini_def());
716
717        let program = cx.compile().unwrap();
718
719        assert_eq!(mounted.offset(), 0);
720        assert_eq!(program.num_columns(), 2);
721        assert!(program.permutation_checks().is_empty());
722        assert_eq!(program.fixed_columns().len(), 1);
723    }
724
725    #[test]
726    fn plain_columns_elide_expander() {
727        let mut cx = Circuit::<F>::new("plain", 16).unwrap();
728        cx.columns(3, ColumnType::B64);
729        cx.column(ColumnType::Bit);
730
731        let program = cx.compile().unwrap();
732
733        assert!(program.virtual_expander().is_none());
734        assert_eq!(program.num_columns(), 4);
735        assert_eq!(program.column_layout(), program.virtual_column_layout());
736    }
737
738    #[test]
739    fn publish_assigns_sequential_slots() {
740        let mut cx = Circuit::<F>::new("slots", 16).unwrap();
741        let val = cx.column(ColumnType::B32);
742
743        let s0 = cx.publish(val, 3);
744        let s1 = cx.publish(val, 5);
745
746        assert_eq!((s0, s1), (0, 1));
747
748        let program = cx.compile().unwrap();
749
750        assert_eq!(program.num_public_inputs(), 2);
751
752        let boundaries = program.boundary_constraints();
753        assert_eq!(boundaries.len(), 2);
754        assert_eq!(boundaries[0].target, BoundaryTarget::PublicInput(0));
755        assert_eq!(boundaries[1].target, BoundaryTarget::PublicInput(1));
756    }
757
758    #[test]
759    fn compile_rejects_witness_bus_selector() {
760        let mut cx = Circuit::<F>::new("discipline", 16).unwrap();
761        let key = cx.column(ColumnType::B32);
762        let sel = cx.column(ColumnType::Bit);
763
764        cx.bus("lone_bus", clocked_spec(key.index(), sel.index()));
765
766        assert!(cx.compile().is_err());
767    }
768
769    #[test]
770    fn compile_keeps_pinned_selector_unrooted() {
771        let mut cx = Circuit::<F>::new("pinned", 16).unwrap();
772        let key = cx.column(ColumnType::B32);
773        let sel = cx.column(ColumnType::Bit);
774
775        cx.fix(sel, FixedShape::Sparse(vec![(0, F::ONE)]));
776        cx.bus("lone_bus", clocked_spec(key.index(), sel.index()));
777
778        assert!(cx.compile().unwrap().constraint_ast().roots.is_empty());
779    }
780
781    #[test]
782    fn compile_rejects_out_of_range_bus_source() {
783        let mut cx = Circuit::<F>::new("oor", 16).unwrap();
784        let sel = cx.column(ColumnType::Bit);
785
786        cx.bus("bad_bus", clocked_spec(99, sel.index()));
787
788        assert!(cx.compile().is_err());
789    }
790
791    #[test]
792    fn mounted_expander_is_kept_and_offset() {
793        let expander_def = {
794            #[derive(Clone)]
795            struct Packed {
796                expander: VirtualExpander,
797            }
798
799            impl Air<F> for Packed {
800                fn num_columns(&self) -> usize {
801                    self.expander.num_virtual_columns()
802                }
803
804                fn column_layout(&self) -> &[ColumnType] {
805                    &[ColumnType::B32, ColumnType::Bit]
806                }
807
808                fn virtual_expander(&self) -> Option<&VirtualExpander> {
809                    Some(&self.expander)
810                }
811
812                fn constraint_ast(&self) -> ConstraintAst<F> {
813                    ConstraintSystem::<F>::new().build()
814                }
815            }
816
817            ChipletDef::from_air(&Packed {
818                expander: VirtualExpander::new()
819                    .expand_bits(1, ColumnType::B32)
820                    .control_bits(1)
821                    .build()
822                    .unwrap(),
823            })
824            .unwrap()
825        };
826
827        let mut cx = Circuit::<F>::new("packed_host", 16).unwrap();
828
829        let host = cx.column(ColumnType::B64);
830        let mounted = cx.mount(expander_def);
831
832        assert_eq!(host.index(), 0);
833        assert_eq!(mounted.offset(), 1);
834        assert_eq!(mounted.num_columns(), 33);
835
836        let program = cx.compile().unwrap();
837
838        assert!(program.virtual_expander().is_some());
839        assert_eq!(program.num_columns(), 34);
840        assert_eq!(program.virtual_column_layout()[0], ColumnType::B64);
841        assert_eq!(program.virtual_column_layout()[1], ColumnType::Bit);
842        assert_eq!(program.virtual_column_layout()[33], ColumnType::Bit);
843    }
844}