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 alloc::string::String;
13use alloc::vec::Vec;
14use core::ops::Range;
15use hekate_core::errors;
16use hekate_core::trace::ColumnType;
17use hekate_math::{HardwareField, TowerField};
18
19use crate::chiplet::ChipletDef;
20use crate::constraint::builder::ConstraintSystem;
21use crate::constraint::{BoundaryConstraint, BoundaryTarget, ConstraintAst};
22use crate::expander::VirtualExpander;
23use crate::permutation::{
24    PermutationCheckSpec, RankTable, Service, Source, TableHeight, validate_bus_set,
25    validate_fixed_selectors, validate_ordered_emits,
26};
27use crate::{Air, FixedColumn, FixedShape, InlineKernelHint, Program, validate_fixed_columns};
28
29/// Virtual column handle issued by `Circuit`.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct Col(usize);
32
33impl Col {
34    #[inline(always)]
35    pub fn index(self) -> usize {
36        self.0
37    }
38}
39
40/// Contiguous run of virtual columns.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct ColRange {
43    start: usize,
44    count: usize,
45}
46
47impl ColRange {
48    pub fn at(self, i: usize) -> Col {
49        assert!(i < self.count, "ColRange::at out of range");
50        Col(self.start + i)
51    }
52
53    #[inline(always)]
54    pub fn start(self) -> usize {
55        self.start
56    }
57
58    #[inline(always)]
59    pub fn len(self) -> usize {
60        self.count
61    }
62
63    #[inline(always)]
64    pub fn is_empty(self) -> bool {
65        self.count == 0
66    }
67
68    pub fn iter(self) -> impl Iterator<Item = Col> {
69        (self.start..self.start + self.count).map(Col)
70    }
71}
72
73/// Handle to a bit-expanded physical column group.
74#[derive(Clone, Copy, Debug)]
75pub struct Packed {
76    phys_start: usize,
77    count: usize,
78    bits_per: usize,
79    bits: ColRange,
80}
81
82impl Packed {
83    /// Bit columns of the group's `i`-th physical column.
84    ///
85    /// # Panics
86    /// `i >= count`.
87    pub fn bits(&self, i: usize) -> ColRange {
88        assert!(i < self.count, "Packed::bits out of range");
89        ColRange {
90            start: self.bits.start + i * self.bits_per,
91            count: self.bits_per,
92        }
93    }
94}
95
96/// Contiguous run of committed columns.
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub struct PhysRange {
99    start: usize,
100    count: usize,
101}
102
103impl PhysRange {
104    /// Columns `[start, start + count)` of this run.
105    pub fn slice(self, start: usize, count: usize) -> PhysRange {
106        assert!(
107            start
108                .checked_add(count)
109                .is_some_and(|end| end <= self.count),
110            "PhysRange::slice out of range"
111        );
112
113        PhysRange {
114            start: self.start + start,
115            count,
116        }
117    }
118
119    #[inline(always)]
120    pub fn len(self) -> usize {
121        self.count
122    }
123
124    #[inline(always)]
125    pub fn is_empty(self) -> bool {
126        self.count == 0
127    }
128}
129
130impl From<&Packed> for PhysRange {
131    fn from(packed: &Packed) -> Self {
132        PhysRange {
133            start: packed.phys_start,
134            count: packed.count,
135        }
136    }
137}
138
139/// Handle to an inline-mounted chiplet.
140#[derive(Clone, Copy, Debug)]
141pub struct Mounted {
142    offset: usize,
143    num_columns: usize,
144    physical: PhysRange,
145}
146
147impl Mounted {
148    /// # Panics
149    /// `local >= num_columns()`.
150    pub fn col(&self, local: usize) -> Col {
151        assert!(local < self.num_columns, "Mounted::col out of range");
152        Col(self.offset + local)
153    }
154
155    #[inline(always)]
156    pub fn offset(&self) -> usize {
157        self.offset
158    }
159
160    #[inline(always)]
161    pub fn num_columns(&self) -> usize {
162        self.num_columns
163    }
164
165    /// The chiplet's committed columns, in its `column_layout()` order.
166    /// A reuse view must slice them within one expander entry.
167    #[inline(always)]
168    pub fn physical(&self) -> PhysRange {
169        self.physical
170    }
171}
172
173/// Recorder for one program: main-table declarations
174/// plus mounted and attached chiplets.
175pub struct Circuit<F: TowerField> {
176    name: String,
177    num_rows: usize,
178    cs: ConstraintSystem<F>,
179    physical: Vec<ColumnType>,
180    expander: VirtualExpander,
181    pending_run: Option<(ColumnType, usize)>,
182    num_virtual: usize,
183    fixed: Vec<FixedColumn<F>>,
184    boundaries: Vec<BoundaryConstraint<F>>,
185    buses: Vec<(String, PermutationCheckSpec)>,
186    num_public_inputs: usize,
187    inline_chiplets: Vec<ChipletDef<F>>,
188    inline_hints: Vec<InlineKernelHint>,
189    chiplet_defs: Vec<ChipletDef<F>>,
190    namespaces: Vec<(String, Range<usize>)>,
191}
192
193impl<F: TowerField + HardwareField> Circuit<F> {
194    /// # Errors
195    /// `num_rows` not a power of two.
196    pub fn new(name: &str, num_rows: usize) -> errors::Result<Self> {
197        if !num_rows.is_power_of_two() {
198            return Err(errors::Error::Protocol {
199                protocol: "circuit",
200                message: "num_rows must be a power of two",
201            });
202        }
203
204        Ok(Self {
205            name: String::from(name),
206            num_rows,
207            cs: ConstraintSystem::new(),
208            physical: Vec::new(),
209            expander: VirtualExpander::new(),
210            pending_run: None,
211            num_virtual: 0,
212            fixed: Vec::new(),
213            boundaries: Vec::new(),
214            buses: Vec::new(),
215            num_public_inputs: 0,
216            inline_chiplets: Vec::new(),
217            inline_hints: Vec::new(),
218            chiplet_defs: Vec::new(),
219            namespaces: Vec::new(),
220        })
221    }
222
223    #[inline(always)]
224    pub fn num_rows(&self) -> usize {
225        self.num_rows
226    }
227
228    /// Constraint DSL over the recorded columns.
229    /// Column handles convert via `Col::index`.
230    pub fn cs(&self) -> &ConstraintSystem<F> {
231        &self.cs
232    }
233
234    pub fn column(&mut self, ty: ColumnType) -> Col {
235        self.columns(1, ty).at(0)
236    }
237
238    /// Records a `define_columns!` layout 1:1, physical to
239    /// virtual. Index the range with the schema's own constants.
240    pub fn schema(&mut self, layout: &[ColumnType]) -> ColRange {
241        let start = self.num_virtual;
242
243        for &ty in layout {
244            self.column(ty);
245        }
246
247        ColRange {
248            start,
249            count: layout.len(),
250        }
251    }
252
253    pub fn columns(&mut self, count: usize, ty: ColumnType) -> ColRange {
254        let start = self.num_virtual;
255
256        self.physical.extend(core::iter::repeat_n(ty, count));
257
258        self.num_virtual += count;
259
260        match self.pending_run.take() {
261            Some((run_ty, run_count)) if run_ty == ty => {
262                self.pending_run = Some((ty, run_count + count));
263            }
264            prior => {
265                self.flush_run(prior);
266
267                self.pending_run = Some((ty, count));
268            }
269        }
270
271        ColRange { start, count }
272    }
273
274    /// Commits `count` `storage` columns that constraints see
275    /// whole, as one expander entry: `columns` merges adjacent
276    /// same-type runs and the entries enter `program_id`.
277    pub fn pass_through(&mut self, count: usize, storage: ColumnType) -> ColRange {
278        self.flush_pending();
279
280        let cols = ColRange {
281            start: self.num_virtual,
282            count,
283        };
284
285        self.physical.extend(core::iter::repeat_n(storage, count));
286
287        self.num_virtual += count;
288
289        let expander = core::mem::take(&mut self.expander);
290
291        self.expander = expander.pass_through(count, storage);
292
293        cols
294    }
295
296    /// Commits `count` `storage` columns (`B8` to `B64`) that
297    /// constraints see as bits: `bits(i).at(k)` is bit `k` of
298    /// column `i`. `reuse_pass_through` also shows them whole.
299    pub fn expand_bits(&mut self, count: usize, storage: ColumnType) -> Packed {
300        self.flush_pending();
301
302        let phys_start = self.physical.len();
303        let bits_per = storage.byte_size() * 8;
304        let bits = ColRange {
305            start: self.num_virtual,
306            count: count * bits_per,
307        };
308
309        self.physical.extend(core::iter::repeat_n(storage, count));
310
311        self.num_virtual += bits.count;
312
313        let expander = core::mem::take(&mut self.expander);
314
315        self.expander = expander.expand_bits(count, storage);
316
317        Packed {
318            phys_start,
319            count,
320            bits_per,
321            bits,
322        }
323    }
324
325    /// Lets constraints read committed columns `src` whole
326    /// without committing them again: the `Packed` from
327    /// `expand_bits` or a slice of `Mounted::physical()`.
328    pub fn reuse_pass_through(&mut self, src: impl Into<PhysRange>) -> ColRange {
329        let src = src.into();
330
331        self.flush_pending();
332
333        let view = ColRange {
334            start: self.num_virtual,
335            count: src.count,
336        };
337
338        self.num_virtual += src.count;
339
340        let expander = core::mem::take(&mut self.expander);
341
342        self.expander = expander.reuse_pass_through(src.start, src.count);
343
344        view
345    }
346
347    /// Lets constraints read committed columns `src` as bits
348    /// without committing them again. Bit numbering and the
349    /// `B8` to `B64` limit are those of `expand_bits`.
350    pub fn reuse_expand_bits(&mut self, src: impl Into<PhysRange>) -> Packed {
351        let src = src.into();
352
353        self.flush_pending();
354
355        let bits_per = self
356            .physical
357            .get(src.start)
358            .map_or(0, |storage| storage.byte_size() * 8);
359
360        let bits = ColRange {
361            start: self.num_virtual,
362            count: src.count * bits_per,
363        };
364
365        self.num_virtual += bits.count;
366
367        let expander = core::mem::take(&mut self.expander);
368
369        self.expander = expander.reuse_expand_bits(src.start, src.count);
370
371        Packed {
372            phys_start: src.start,
373            count: src.count,
374            bits_per,
375            bits,
376        }
377    }
378
379    /// Committed columns behind `cols`, a range of
380    /// declared columns or of a whole-column view.
381    pub fn physical(&self, cols: ColRange) -> errors::Result<PhysRange> {
382        let pending = self.pending_run.map_or(0, |(_, count)| count);
383        let flushed = self.num_virtual - pending;
384        let tail = self.physical.len() - pending;
385
386        let whole = |virt: usize| match virt {
387            v if v >= self.num_virtual => None,
388            v if v >= flushed => Some(tail + (v - flushed)),
389            v => self.expander.whole_column(v),
390        };
391
392        let start = whole(cols.start)
393            .filter(|&start| (1..cols.count).all(|i| whole(cols.start + i) == Some(start + i)));
394
395        match start {
396            Some(start) => Ok(PhysRange {
397                start,
398                count: cols.count,
399            }),
400            None => Err(errors::Error::Protocol {
401                protocol: "circuit",
402                message: "columns are not one run of whole committed columns",
403            }),
404        }
405    }
406
407    /// Pins `col` to `shape`; the verifier evaluates
408    /// the shape's MLE for itself at `r_final`.
409    pub fn fix(&mut self, col: Col, shape: FixedShape<F>) {
410        self.fixed.push(FixedColumn {
411            col_idx: col.index(),
412            shape,
413        });
414    }
415
416    /// Constant-target pin;
417    /// public inputs enter only through `publish`.
418    pub fn boundary(&mut self, col: Col, row: usize, value: F) {
419        self.boundaries.push(BoundaryConstraint {
420            col_idx: col.index(),
421            row_idx: row,
422            target: BoundaryTarget::Constant(value),
423        });
424    }
425
426    /// Requester endpoint of `service` on this table.
427    pub fn call(&mut self, service: &Service, values: &[Col], selector: Col) -> errors::Result<()> {
428        let cols: Vec<usize> = values.iter().map(|c| c.index()).collect();
429        let spec = service.request(&cols, selector.index())?;
430
431        self.buses.push((String::from(service.bus_id), spec));
432
433        Ok(())
434    }
435
436    /// Raw bus endpoint for shapes `Service` does
437    /// not model (positional, paired, waivered).
438    pub fn bus(&mut self, bus_id: &str, spec: PermutationCheckSpec) {
439        self.buses.push((String::from(bus_id), spec));
440    }
441
442    /// Mounts a chiplet inline: its artifacts and bus endpoints
443    /// merge into this table at engine-computed offsets.
444    pub fn mount(&mut self, def: ChipletDef<F>) -> Mounted {
445        self.mount_inner(def, true)
446    }
447
448    /// Mounts a chiplet used as the program's own
449    /// trace, whose declared bus has no partner here.
450    pub fn mount_unlinked(&mut self, def: ChipletDef<F>) -> Mounted {
451        self.mount_inner(def, false)
452    }
453
454    /// Attaches a chiplet as an independent table
455    /// with its own trace, commitment, and ZeroCheck.
456    pub fn attach(&mut self, def: ChipletDef<F>) {
457        self.chiplet_defs.push(def);
458    }
459
460    /// Attaches `defs` as independent tables, renaming every bus
461    /// but `external` to `"{namespace}::{id}"`: two attached
462    /// groups whose buses share an id cannot trade tokens.
463    pub fn attach_namespaced(
464        &mut self,
465        namespace: &str,
466        defs: Vec<ChipletDef<F>>,
467        external: &[&str],
468    ) -> errors::Result<()> {
469        if namespace.is_empty()
470            || namespace.contains(':')
471            || self.namespaces.iter().any(|(n, _)| n == namespace)
472        {
473            return Err(errors::Error::Protocol {
474                protocol: "circuit",
475                message: "namespace is empty, holds ':' or is already attached",
476            });
477        }
478
479        let unknown = external.iter().any(|&id| {
480            !defs
481                .iter()
482                .any(|def| def.permutation_checks().iter().any(|(bus, _)| bus == id))
483        });
484
485        if unknown {
486            return Err(errors::Error::Protocol {
487                protocol: "circuit",
488                message: "external bus id names no bus of the attached tables",
489            });
490        }
491
492        let exempt: Vec<String> = external.iter().map(|&id| String::from(id)).collect();
493        let start = self.chiplet_defs.len();
494
495        for mut def in defs {
496            def.prefix_bus_ids(namespace, &exempt);
497            self.chiplet_defs.push(def);
498        }
499
500        self.namespaces
501            .push((String::from(namespace), start..self.chiplet_defs.len()));
502
503        Ok(())
504    }
505
506    /// Pins `col` at `row` to a fresh public input slot,
507    /// returned. Whether the AIR determines the pinned
508    /// cell is the audited circuit's burden, not checked.
509    pub fn publish(&mut self, col: Col, row: usize) -> usize {
510        let slot = self.next_slot();
511
512        self.boundaries.push(BoundaryConstraint::with_public_input(
513            col.index(),
514            row,
515            slot,
516        ));
517
518        slot
519    }
520
521    /// Derives the program artifacts, running every
522    /// structural validator on the recording.
523    pub fn compile(mut self) -> errors::Result<CircuitProgram<F>> {
524        self.flush_pending();
525
526        let expander = self.expander.build()?;
527        let width = self.num_virtual;
528        let num_vars = self.num_rows.trailing_zeros() as usize;
529
530        if expander.num_virtual_columns() != width {
531            return Err(errors::Error::Protocol {
532                protocol: "circuit",
533                message: "expander virtual width diverged from declared columns",
534            });
535        }
536
537        let one_to_one = expander.num_virtual_columns() == expander.num_physical_columns();
538        let virtual_layout = expander.virtual_layout().to_vec();
539
540        let ast = self.cs.build();
541
542        for (bus_id, spec) in &self.buses {
543            spec.validate_clock_stitching(bus_id)?;
544
545            for (source, _) in &spec.sources {
546                validate_source_range(source, width)?;
547            }
548        }
549
550        validate_fixed_selectors(&self.buses, &self.fixed)?;
551
552        for bc in &self.boundaries {
553            if bc.col_idx >= width || bc.row_idx >= self.num_rows {
554                return Err(errors::Error::Protocol {
555                    protocol: "circuit",
556                    message: "boundary constraint outside the trace",
557                });
558            }
559        }
560
561        validate_fixed_columns(&self.fixed, &virtual_layout, Some(num_vars))?;
562
563        for bc in &self.boundaries {
564            if let BoundaryTarget::PublicInput(slot) = bc.target
565                && slot >= self.num_public_inputs
566            {
567                return Err(errors::Error::Protocol {
568                    protocol: "circuit",
569                    message: "boundary public input slot out of range",
570                });
571            }
572        }
573
574        let chiplet_specs: Vec<Vec<(String, PermutationCheckSpec)>> = self
575            .chiplet_defs
576            .iter()
577            .map(|cd| cd.permutation_checks())
578            .collect();
579
580        for (namespace, group) in &self.namespaces {
581            let mut prefix = namespace.clone();
582            prefix.push_str("::");
583
584            let outside = chiplet_specs
585                .iter()
586                .enumerate()
587                .filter(|(i, _)| !group.contains(i))
588                .flat_map(|(_, specs)| specs)
589                .chain(&self.buses)
590                .any(|(id, _)| id.starts_with(&prefix));
591
592            if outside {
593                return Err(errors::Error::Protocol {
594                    protocol: "circuit",
595                    message: "bus outside a namespace carries its prefix",
596                });
597            }
598        }
599
600        let endpoints = self
601            .buses
602            .iter()
603            .chain(chiplet_specs.iter().flatten())
604            .map(|(id, s)| (id.as_str(), s));
605
606        validate_bus_set(endpoints)?;
607
608        let mut rank_tables = Vec::with_capacity(1 + self.chiplet_defs.len());
609
610        rank_tables.push(RankTable {
611            specs: &self.buses,
612            fixed: &self.fixed,
613            height: TableHeight::Main(num_vars),
614        });
615
616        for (def, specs) in self.chiplet_defs.iter().zip(&chiplet_specs) {
617            rank_tables.push(RankTable {
618                specs,
619                fixed: def.pins(),
620                height: TableHeight::Chiplet(None),
621            });
622        }
623
624        validate_ordered_emits(&rank_tables)?;
625
626        let table = ChipletDef::from_parts(
627            self.name,
628            width,
629            ast,
630            self.physical,
631            virtual_layout,
632            self.boundaries,
633            self.fixed,
634            (!one_to_one).then_some(expander),
635            self.buses,
636        );
637
638        Ok(CircuitProgram {
639            table,
640            num_public_inputs: self.num_public_inputs,
641            inline_chiplets: self.inline_chiplets,
642            inline_kernels: self.inline_hints,
643            chiplet_defs: self.chiplet_defs,
644        })
645    }
646
647    fn mount_inner(&mut self, def: ChipletDef<F>, link: bool) -> Mounted {
648        self.flush_pending();
649
650        let chiplet_idx = self.inline_chiplets.len();
651        let offset = self.num_virtual;
652        let num_columns = Air::<F>::num_columns(&def);
653
654        let physical = PhysRange {
655            start: self.physical.len(),
656            count: def.column_layout().len(),
657        };
658
659        let mut ast = def.constraint_ast();
660        ast.arena.shift_cells(offset);
661
662        self.inline_hints.push(InlineKernelHint {
663            chiplet_idx,
664            root_offset: self.cs.num_roots(),
665            column_offset: offset,
666        });
667
668        self.cs.merge_ast(ast);
669
670        for mut fc in Air::<F>::fixed_columns(&def) {
671            fc.col_idx += offset;
672
673            self.fixed.push(fc);
674        }
675
676        for mut bc in def.boundary_constraints() {
677            bc.col_idx += offset;
678
679            self.boundaries.push(bc);
680        }
681
682        match def.virtual_expander() {
683            Some(e) => {
684                let expander = core::mem::take(&mut self.expander);
685
686                self.expander = expander.append(e);
687
688                self.physical.extend_from_slice(def.column_layout());
689
690                self.num_virtual += num_columns;
691            }
692            None => {
693                self.schema(def.column_layout());
694            }
695        }
696
697        if link {
698            for (bus_id, mut spec) in def.permutation_checks() {
699                spec.shift_column_indices(offset);
700                self.buses.push((bus_id, spec));
701            }
702        }
703
704        self.inline_chiplets.push(def);
705
706        Mounted {
707            offset,
708            num_columns,
709            physical,
710        }
711    }
712
713    fn next_slot(&mut self) -> usize {
714        let slot = self.num_public_inputs;
715        self.num_public_inputs += 1;
716
717        slot
718    }
719
720    fn flush_pending(&mut self) {
721        let prior = self.pending_run.take();
722        self.flush_run(prior);
723    }
724
725    fn flush_run(&mut self, run: Option<(ColumnType, usize)>) {
726        let Some((ty, count)) = run else {
727            return;
728        };
729
730        let expander = core::mem::take(&mut self.expander);
731
732        self.expander = match ty {
733            ColumnType::Bit => expander.control_bits(count),
734            _ => expander.pass_through(count, ty),
735        };
736    }
737}
738
739/// A `Circuit`'s compiled program.
740#[derive(Clone)]
741pub struct CircuitProgram<F: TowerField> {
742    table: ChipletDef<F>,
743    num_public_inputs: usize,
744    inline_chiplets: Vec<ChipletDef<F>>,
745    inline_kernels: Vec<InlineKernelHint>,
746    chiplet_defs: Vec<ChipletDef<F>>,
747}
748
749impl<F: TowerField> Air<F> for CircuitProgram<F> {
750    fn name(&self) -> String {
751        self.table.name()
752    }
753
754    fn num_columns(&self) -> usize {
755        self.table.num_columns()
756    }
757
758    fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
759        self.table.boundary_constraints()
760    }
761
762    fn column_layout(&self) -> &[ColumnType] {
763        self.table.column_layout()
764    }
765
766    fn virtual_column_layout(&self) -> &[ColumnType] {
767        self.table.virtual_column_layout()
768    }
769
770    fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
771        self.table.permutation_checks()
772    }
773
774    fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
775        self.table.fixed_columns()
776    }
777
778    fn virtual_expander(&self) -> Option<&VirtualExpander> {
779        self.table.virtual_expander()
780    }
781
782    fn constraint_ast(&self) -> ConstraintAst<F> {
783        self.table.constraint_ast()
784    }
785
786    fn inline_chiplets(&self) -> errors::Result<Vec<ChipletDef<F>>> {
787        Ok(self.inline_chiplets.clone())
788    }
789
790    fn inline_chiplet_kernels(&self) -> Vec<InlineKernelHint> {
791        self.inline_kernels.clone()
792    }
793}
794
795impl<F: TowerField> Program<F> for CircuitProgram<F> {
796    fn num_public_inputs(&self) -> usize {
797        self.num_public_inputs
798    }
799
800    fn chiplet_defs(&self) -> errors::Result<Vec<ChipletDef<F>>> {
801        Ok(self.chiplet_defs.clone())
802    }
803}
804
805fn validate_source_range(source: &Source, width: usize) -> errors::Result<()> {
806    let out_of_range = match source {
807        Source::Column(idx) | Source::PhaseColumn(idx) => *idx >= width,
808        Source::Columns(indices) => indices.iter().any(|&idx| idx >= width),
809        Source::RowIndexLeBytes(_)
810        | Source::RowIndexByte(_)
811        | Source::Const(_)
812        | Source::EmitRank(_) => false,
813    };
814
815    if out_of_range {
816        return Err(errors::Error::Protocol {
817            protocol: "circuit",
818            message: "bus source column out of range",
819        });
820    }
821
822    Ok(())
823}
824
825#[cfg(test)]
826mod tests {
827    use super::*;
828    use crate::digest::program_id;
829    use crate::permutation::{BusKind, EMIT_RANK_LABEL, ServiceSlot, Side, Source};
830    use alloc::string::ToString;
831    use alloc::vec;
832    use hekate_math::Block128;
833
834    type F = Block128;
835
836    #[derive(Clone)]
837    struct MiniChiplet;
838
839    impl Air<F> for MiniChiplet {
840        fn name(&self) -> String {
841            "MiniChiplet".to_string()
842        }
843
844        fn num_columns(&self) -> usize {
845            2
846        }
847
848        fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
849            vec![BoundaryConstraint::with_constant(0, 0, F::ZERO)]
850        }
851
852        fn column_layout(&self) -> &[ColumnType] {
853            &[ColumnType::B32, ColumnType::Bit]
854        }
855
856        fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
857            vec![(
858                "mini_bus".to_string(),
859                PermutationCheckSpec::new(
860                    vec![
861                        (Source::Column(0), b"k_v"),
862                        (Source::RowIndexLeBytes(4), b"k_clk"),
863                    ],
864                    Some(1),
865                ),
866            )]
867        }
868
869        fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
870            vec![FixedColumn::prefix(1, 1)]
871        }
872
873        fn constraint_ast(&self) -> ConstraintAst<F> {
874            let cs = ConstraintSystem::<F>::new();
875            cs.assert_boolean(cs.col(1));
876
877            cs.build()
878        }
879    }
880
881    #[derive(Clone)]
882    struct Server {
883        calls: usize,
884        spec: PermutationCheckSpec,
885    }
886
887    impl Air<F> for Server {
888        fn name(&self) -> String {
889            "Server".to_string()
890        }
891
892        fn num_columns(&self) -> usize {
893            3
894        }
895
896        fn column_layout(&self) -> &[ColumnType] {
897            &[ColumnType::B32, ColumnType::Bit, ColumnType::B32]
898        }
899
900        fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
901            vec![("svc".to_string(), self.spec.clone())]
902        }
903
904        fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
905            vec![FixedColumn {
906                col_idx: 1,
907                shape: two_emit_calls(self.calls),
908            }]
909        }
910
911        fn constraint_ast(&self) -> ConstraintAst<F> {
912            ConstraintSystem::<F>::new().build()
913        }
914    }
915
916    fn mini_def() -> ChipletDef<F> {
917        ChipletDef::from_air(&MiniChiplet).unwrap()
918    }
919
920    fn clocked_spec(key_col: usize, selector: usize) -> PermutationCheckSpec {
921        PermutationCheckSpec::new(
922            vec![
923                (Source::Column(key_col), b"k_v"),
924                (Source::RowIndexLeBytes(4), b"k_clk"),
925            ],
926            Some(selector),
927        )
928    }
929
930    fn ordered_service() -> Service {
931        Service {
932            bus_id: "svc",
933            kind: BusKind::Permutation,
934            slots: vec![ServiceSlot::Value(b"k_v"), ServiceSlot::EmitRank],
935        }
936    }
937
938    fn two_emit_calls(calls: usize) -> FixedShape<F> {
939        FixedShape::Cadence {
940            stride: 4,
941            count: calls,
942            origin: 0,
943            values: vec![F::ONE, F::ZERO, F::ZERO, F::ONE],
944        }
945    }
946
947    fn ordered_host(server: Server) -> errors::Result<CircuitProgram<F>> {
948        let mut cx = Circuit::<F>::new("ordered_host", 16)?;
949
950        let value = cx.column(ColumnType::B32);
951        let sel = cx.column(ColumnType::Bit);
952
953        cx.fix(sel, two_emit_calls(2));
954        cx.call(&ordered_service(), &[value], sel)?;
955        cx.attach(ChipletDef::from_air(&server)?);
956
957        cx.compile()
958    }
959
960    fn rejected_by(res: errors::Result<CircuitProgram<F>>, prefix: &str) -> bool {
961        matches!(res, Err(errors::Error::Protocol { message, .. }) if message.starts_with(prefix))
962    }
963
964    #[test]
965    fn mount_matches_manual_shift() {
966        let mut cx = Circuit::<F>::new("mini_host", 16).unwrap();
967        let host_key = cx.column(ColumnType::B32);
968
969        let cs = cx.cs();
970        cs.constrain(cs.col(host_key.index()) * cs.col(host_key.index()));
971
972        let mounted = cx.mount(mini_def());
973
974        assert_eq!(mounted.offset(), 1);
975
976        let program = cx.compile().unwrap();
977
978        assert_eq!(
979            program.column_layout(),
980            &[ColumnType::B32, ColumnType::B32, ColumnType::Bit]
981        );
982
983        let hints = program.inline_chiplet_kernels();
984
985        assert_eq!(hints.len(), 1);
986        assert_eq!(hints[0].chiplet_idx, 0);
987        assert_eq!(hints[0].root_offset, 1);
988        assert_eq!(hints[0].column_offset, 1);
989
990        let mut manual = {
991            let cs = ConstraintSystem::<F>::new();
992            cs.constrain(cs.col(0) * cs.col(0));
993
994            cs.build()
995        };
996
997        let mut chiplet_ast = Air::<F>::constraint_ast(&MiniChiplet);
998        chiplet_ast.arena.shift_cells(1);
999
1000        manual.merge(chiplet_ast);
1001
1002        assert_eq!(
1003            program.constraint_ast().to_constraints(),
1004            manual.to_constraints()
1005        );
1006
1007        let specs = program.permutation_checks();
1008
1009        assert_eq!(specs.len(), 1);
1010        assert_eq!(specs[0].1.selector, Some(2));
1011        assert_eq!(specs[0].1.sources[0].0, Source::Column(1));
1012
1013        let pins = program.fixed_columns();
1014
1015        assert_eq!(pins.len(), 1);
1016        assert_eq!(pins[0].col_idx, 2);
1017
1018        let boundaries = program.boundary_constraints();
1019
1020        assert_eq!(boundaries.len(), 1);
1021        assert_eq!(boundaries[0].col_idx, 1);
1022    }
1023
1024    #[test]
1025    fn mount_unlinked_keeps_columns_and_drops_endpoint() {
1026        let mut cx = Circuit::<F>::new("isolated_host", 16).unwrap();
1027        let mounted = cx.mount_unlinked(mini_def());
1028
1029        let program = cx.compile().unwrap();
1030
1031        assert_eq!(mounted.offset(), 0);
1032        assert_eq!(program.num_columns(), 2);
1033        assert!(program.permutation_checks().is_empty());
1034        assert_eq!(program.fixed_columns().len(), 1);
1035    }
1036
1037    #[test]
1038    fn plain_columns_elide_expander() {
1039        let mut cx = Circuit::<F>::new("plain", 16).unwrap();
1040        cx.columns(3, ColumnType::B64);
1041        cx.column(ColumnType::Bit);
1042
1043        let program = cx.compile().unwrap();
1044
1045        assert!(program.virtual_expander().is_none());
1046        assert_eq!(program.num_columns(), 4);
1047        assert_eq!(program.column_layout(), program.virtual_column_layout());
1048    }
1049
1050    #[test]
1051    fn publish_assigns_sequential_slots() {
1052        let mut cx = Circuit::<F>::new("slots", 16).unwrap();
1053        let val = cx.column(ColumnType::B32);
1054
1055        let s0 = cx.publish(val, 3);
1056        let s1 = cx.publish(val, 5);
1057
1058        assert_eq!((s0, s1), (0, 1));
1059
1060        let program = cx.compile().unwrap();
1061
1062        assert_eq!(program.num_public_inputs(), 2);
1063
1064        let boundaries = program.boundary_constraints();
1065        assert_eq!(boundaries.len(), 2);
1066        assert_eq!(boundaries[0].target, BoundaryTarget::PublicInput(0));
1067        assert_eq!(boundaries[1].target, BoundaryTarget::PublicInput(1));
1068    }
1069
1070    #[test]
1071    fn compile_rejects_witness_bus_selector() {
1072        let mut cx = Circuit::<F>::new("discipline", 16).unwrap();
1073        let key = cx.column(ColumnType::B32);
1074        let sel = cx.column(ColumnType::Bit);
1075
1076        cx.bus("lone_bus", clocked_spec(key.index(), sel.index()));
1077
1078        assert!(cx.compile().is_err());
1079    }
1080
1081    #[test]
1082    fn compile_keeps_pinned_selector_unrooted() {
1083        let mut cx = Circuit::<F>::new("pinned", 16).unwrap();
1084        let key = cx.column(ColumnType::B32);
1085        let sel = cx.column(ColumnType::Bit);
1086
1087        cx.fix(sel, FixedShape::Sparse(vec![(0, F::ONE)]));
1088        cx.bus("lone_bus", clocked_spec(key.index(), sel.index()));
1089
1090        assert!(cx.compile().unwrap().constraint_ast().roots.is_empty());
1091    }
1092
1093    #[test]
1094    fn compile_rejects_out_of_range_bus_source() {
1095        let mut cx = Circuit::<F>::new("oor", 16).unwrap();
1096        let sel = cx.column(ColumnType::Bit);
1097
1098        cx.bus("bad_bus", clocked_spec(99, sel.index()));
1099
1100        assert!(cx.compile().is_err());
1101    }
1102
1103    #[test]
1104    fn mounted_expander_is_kept_and_offset() {
1105        let expander_def = {
1106            #[derive(Clone)]
1107            struct Packed {
1108                expander: VirtualExpander,
1109            }
1110
1111            impl Air<F> for Packed {
1112                fn num_columns(&self) -> usize {
1113                    self.expander.num_virtual_columns()
1114                }
1115
1116                fn column_layout(&self) -> &[ColumnType] {
1117                    &[ColumnType::B32, ColumnType::Bit]
1118                }
1119
1120                fn virtual_expander(&self) -> Option<&VirtualExpander> {
1121                    Some(&self.expander)
1122                }
1123
1124                fn constraint_ast(&self) -> ConstraintAst<F> {
1125                    ConstraintSystem::<F>::new().build()
1126                }
1127            }
1128
1129            ChipletDef::from_air(&Packed {
1130                expander: VirtualExpander::new()
1131                    .expand_bits(1, ColumnType::B32)
1132                    .control_bits(1)
1133                    .build()
1134                    .unwrap(),
1135            })
1136            .unwrap()
1137        };
1138
1139        let mut cx = Circuit::<F>::new("packed_host", 16).unwrap();
1140
1141        let host = cx.column(ColumnType::B64);
1142        let mounted = cx.mount(expander_def);
1143
1144        assert_eq!(host.index(), 0);
1145        assert_eq!(mounted.offset(), 1);
1146        assert_eq!(mounted.num_columns(), 33);
1147
1148        let program = cx.compile().unwrap();
1149
1150        assert!(program.virtual_expander().is_some());
1151        assert_eq!(program.num_columns(), 34);
1152        assert_eq!(program.virtual_column_layout()[0], ColumnType::B64);
1153        assert_eq!(program.virtual_column_layout()[1], ColumnType::Bit);
1154        assert_eq!(program.virtual_column_layout()[33], ColumnType::Bit);
1155    }
1156
1157    #[test]
1158    fn mount_keeps_nested_inline_chiplets_on_def() {
1159        let inner = {
1160            let mut cx = Circuit::<F>::new("inner", 16).unwrap();
1161            let own = cx.column(ColumnType::B32);
1162
1163            let cs = cx.cs();
1164            cs.constrain(cs.col(own.index()) * cs.col(own.index()));
1165
1166            cx.mount(mini_def());
1167            cx.compile().unwrap()
1168        };
1169
1170        let def = ChipletDef::from_air(&inner).unwrap();
1171        let nested_hints = vec![InlineKernelHint {
1172            chiplet_idx: 0,
1173            root_offset: 1,
1174            column_offset: 1,
1175        }];
1176
1177        assert_eq!(def.inline_chiplet_kernels(), nested_hints);
1178
1179        let bare = ChipletDef::<F>::from_wire(
1180            Air::<F>::name(&def),
1181            Air::<F>::num_columns(&def),
1182            def.constraint_ast(),
1183            def.column_layout().to_vec(),
1184            def.virtual_column_layout().to_vec(),
1185            def.boundary_constraints(),
1186            Air::<F>::fixed_columns(&def),
1187            def.virtual_expander().cloned(),
1188            def.permutation_checks(),
1189            Vec::new(),
1190            Vec::new(),
1191        )
1192        .unwrap();
1193
1194        let outer = |mounted_def: ChipletDef<F>| {
1195            let mut cx = Circuit::<F>::new("outer", 16).unwrap();
1196            let own = cx.column(ColumnType::B64);
1197
1198            let cs = cx.cs();
1199            cs.constrain(cs.col(own.index()) * cs.col(own.index()));
1200
1201            let mounted = cx.mount(mounted_def);
1202
1203            (mounted, cx.compile().unwrap())
1204        };
1205
1206        let (mounted, program) = outer(def);
1207
1208        assert_eq!(mounted.physical(), PhysRange { start: 1, count: 3 });
1209        assert_eq!(
1210            program.inline_chiplet_kernels(),
1211            vec![InlineKernelHint {
1212                chiplet_idx: 0,
1213                root_offset: 1,
1214                column_offset: 1,
1215            }]
1216        );
1217
1218        let inline_defs = Air::<F>::inline_chiplets(&program).unwrap();
1219
1220        assert_eq!(inline_defs.len(), 1);
1221        assert_eq!(inline_defs[0].inline_hints(), nested_hints);
1222
1223        let mut mini_ast = Air::<F>::constraint_ast(&MiniChiplet);
1224        mini_ast.arena.shift_cells(2);
1225
1226        assert_eq!(
1227            program.constraint_ast().to_constraints()[2..],
1228            mini_ast.to_constraints()[..]
1229        );
1230
1231        assert_eq!(
1232            program_id::<F, _>(&program).unwrap(),
1233            program_id::<F, _>(&outer(bare).1).unwrap()
1234        );
1235
1236        let resnapshot = ChipletDef::from_air(&program).unwrap();
1237
1238        assert_eq!(resnapshot.inline_defs()[0].inline_hints(), nested_hints);
1239    }
1240
1241    #[test]
1242    fn reuse_views_match_hand_built_expander() {
1243        let mut cx = Circuit::<F>::new("views", 16).unwrap();
1244
1245        let lanes = cx.pass_through(3, ColumnType::B64);
1246
1247        cx.pass_through(2, ColumnType::B64);
1248
1249        let flags = cx.schema(&[ColumnType::B16, ColumnType::B32]);
1250        let mounted = cx.mount(mini_def());
1251
1252        let lane_bits = cx.reuse_expand_bits(cx.physical(lanes).unwrap().slice(1, 2));
1253
1254        cx.reuse_expand_bits(cx.physical(flags).unwrap().slice(1, 1));
1255        cx.reuse_expand_bits(mounted.physical().slice(0, 1));
1256        cx.reuse_pass_through(&lane_bits);
1257
1258        let program = cx.compile().unwrap();
1259
1260        let hand = VirtualExpander::new()
1261            .pass_through(3, ColumnType::B64)
1262            .pass_through(2, ColumnType::B64)
1263            .pass_through(1, ColumnType::B16)
1264            .pass_through(1, ColumnType::B32)
1265            .pass_through(1, ColumnType::B32)
1266            .control_bits(1)
1267            .reuse_expand_bits(1, 2)
1268            .reuse_expand_bits(6, 1)
1269            .reuse_expand_bits(7, 1)
1270            .reuse_pass_through(1, 2)
1271            .build()
1272            .unwrap();
1273
1274        let built = program.virtual_expander().unwrap();
1275
1276        assert_eq!(built.expansion_entries(), hand.expansion_entries());
1277        assert_eq!(built.virtual_layout(), hand.virtual_layout());
1278        assert_eq!(program.column_layout().len(), hand.num_physical_columns());
1279    }
1280
1281    #[test]
1282    fn physical_maps_whole_columns_only() {
1283        let mut cx = Circuit::<F>::new("physical", 16).unwrap();
1284
1285        let packed = cx.expand_bits(1, ColumnType::B32);
1286        let pending = cx.columns(2, ColumnType::B64);
1287
1288        assert_eq!(
1289            cx.physical(pending).unwrap(),
1290            PhysRange { start: 1, count: 2 }
1291        );
1292
1293        assert!(cx.physical(packed.bits(0)).is_err());
1294
1295        let view = cx.reuse_pass_through(&packed);
1296
1297        assert_eq!(cx.physical(view).unwrap(), PhysRange { start: 0, count: 1 });
1298    }
1299
1300    #[test]
1301    fn reuse_rejects_range_across_expander_entries() {
1302        let across_schema = {
1303            let mut cx = Circuit::<F>::new("across_schema", 16).unwrap();
1304            let flags = cx.schema(&[ColumnType::B16, ColumnType::B32]);
1305
1306            cx.reuse_pass_through(cx.physical(flags).unwrap());
1307            cx.compile().err()
1308        };
1309
1310        let across_mount = {
1311            let mut cx = Circuit::<F>::new("across_mount", 16).unwrap();
1312            let mounted = cx.mount(mini_def());
1313
1314            cx.reuse_pass_through(mounted.physical());
1315            cx.compile().err()
1316        };
1317
1318        let rejected = Some(errors::Error::Protocol {
1319            protocol: "virtual_expand",
1320            message: "reuse: source columns not found in any single fresh entry",
1321        });
1322
1323        assert_eq!(across_schema, rejected);
1324        assert_eq!(across_mount, rejected);
1325    }
1326
1327    #[test]
1328    fn compile_balances_ordered_bus_sides() {
1329        let responder = |calls| Server {
1330            calls,
1331            spec: ordered_service().respond(&[0], 1).unwrap(),
1332        };
1333
1334        ordered_host(responder(2)).unwrap();
1335
1336        assert!(rejected_by(
1337            ordered_host(responder(3)),
1338            "ordered bus sides emit different counts"
1339        ));
1340    }
1341
1342    #[test]
1343    fn compile_rejects_witness_clock_on_ordered_bus() {
1344        let spec = PermutationCheckSpec::new(
1345            vec![(Source::Column(0), b"k_v"), (Source::Column(2), b"k_clk")],
1346            Some(1),
1347        )
1348        .with_clock_waiver("see circuit.rs: waived to reach the ordered-bus check");
1349
1350        assert!(rejected_by(
1351            ordered_host(Server { calls: 2, spec }),
1352            "ordered bus endpoint has no EmitRank source"
1353        ));
1354    }
1355
1356    #[test]
1357    fn program_id_binds_the_rank_side() {
1358        let program = |first, second| {
1359            let mut cx = Circuit::<F>::new("sides", 16).unwrap();
1360
1361            let value = cx.column(ColumnType::B32);
1362            let sel = cx.column(ColumnType::Bit);
1363
1364            cx.fix(sel, two_emit_calls(2));
1365
1366            for side in [first, second] {
1367                cx.bus(
1368                    "svc",
1369                    PermutationCheckSpec::new(
1370                        vec![
1371                            (Source::Column(value.index()), b"k_v"),
1372                            (Source::EmitRank(side), EMIT_RANK_LABEL),
1373                        ],
1374                        Some(sel.index()),
1375                    ),
1376                );
1377            }
1378
1379            program_id::<F, _>(&cx.compile().unwrap()).unwrap()
1380        };
1381
1382        assert_ne!(
1383            program(Side::Request, Side::Response),
1384            program(Side::Response, Side::Request)
1385        );
1386    }
1387
1388    #[test]
1389    fn attach_namespaced_renames_all_but_external_buses() {
1390        let server = Server {
1391            calls: 2,
1392            spec: ordered_service().respond(&[0], 1).unwrap(),
1393        };
1394
1395        let mut cx = Circuit::<F>::new("namespaced", 16).unwrap();
1396
1397        let value = cx.column(ColumnType::B32);
1398        let sel = cx.column(ColumnType::Bit);
1399
1400        cx.fix(sel, two_emit_calls(2));
1401        cx.call(&ordered_service(), &[value], sel).unwrap();
1402
1403        let defs = vec![ChipletDef::from_air(&server).unwrap(), mini_def()];
1404
1405        cx.attach_namespaced("grp", defs, &["svc"]).unwrap();
1406
1407        let ids: Vec<String> = cx
1408            .compile()
1409            .unwrap()
1410            .chiplet_defs()
1411            .unwrap()
1412            .iter()
1413            .flat_map(|def| def.permutation_checks())
1414            .map(|(id, _)| id)
1415            .collect();
1416
1417        assert_eq!(ids, ["svc", "grp::mini_bus"]);
1418    }
1419
1420    #[test]
1421    fn attach_namespaced_rejects_empty_and_reused_namespaces() {
1422        let mut cx = Circuit::<F>::new("twice", 16).unwrap();
1423
1424        cx.attach_namespaced("grp", vec![mini_def()], &[]).unwrap();
1425
1426        assert!(cx.attach_namespaced("grp", vec![mini_def()], &[]).is_err());
1427        assert!(cx.attach_namespaced("", vec![mini_def()], &[]).is_err());
1428    }
1429
1430    #[test]
1431    fn attach_namespaced_rejects_nested_namespaces_and_unknown_external_ids() {
1432        let mut cx = Circuit::<F>::new("nested", 16).unwrap();
1433
1434        assert_eq!(
1435            cx.attach_namespaced("a::b", vec![mini_def()], &[]).err(),
1436            Some(errors::Error::Protocol {
1437                protocol: "circuit",
1438                message: "namespace is empty, holds ':' or is already attached",
1439            })
1440        );
1441        assert_eq!(
1442            cx.attach_namespaced("grp", vec![mini_def()], &["svc"])
1443                .err(),
1444            Some(errors::Error::Protocol {
1445                protocol: "circuit",
1446                message: "external bus id names no bus of the attached tables",
1447            })
1448        );
1449    }
1450
1451    #[test]
1452    fn bus_outside_namespace_cannot_carry_its_prefix() {
1453        let mut cx = Circuit::<F>::new("prefixed", 16).unwrap();
1454
1455        let value = cx.column(ColumnType::B32);
1456        let sel = cx.column(ColumnType::Bit);
1457
1458        cx.fix(sel, two_emit_calls(2));
1459        cx.bus("grp::mini_bus", clocked_spec(value.index(), sel.index()));
1460
1461        cx.attach_namespaced("grp", vec![mini_def()], &[]).unwrap();
1462
1463        assert_eq!(
1464            cx.compile().err(),
1465            Some(errors::Error::Protocol {
1466                protocol: "circuit",
1467                message: "bus outside a namespace carries its prefix",
1468            })
1469        );
1470    }
1471}