Skip to main content

hekate_program/
lib.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#![cfg_attr(not(feature = "std"), no_std)]
6
7extern crate alloc;
8extern crate core;
9
10use alloc::string::{String, ToString};
11use alloc::vec;
12use alloc::vec::Vec;
13use constraint::{BoundaryConstraint, Constraint, ConstraintAst};
14use core::marker::PhantomData;
15use expander::VirtualExpander;
16use hekate_core::errors;
17use hekate_core::trace::{ColumnTrace, ColumnType, Trace, TraceCompatibleField};
18use hekate_math::{Flat, HardwareField, TowerField};
19use permutation::PermutationCheckSpec;
20
21pub mod chiplet;
22pub mod circuit;
23pub mod constraint;
24pub mod digest;
25pub mod expander;
26pub mod linearized;
27pub mod outer;
28pub mod permutation;
29pub mod predicate;
30pub mod schema;
31
32// =================================================================
33// Core Algebraic Intermediate Representation
34// =================================================================
35
36/// Defines the algebraic structure, trace
37/// layout, and constraints of an AIR table.
38///
39/// Implemented by both standalone
40/// programs and independent chiplets.
41pub trait Air<F: TowerField>: Sized + Clone + Sync {
42    fn name(&self) -> String {
43        "HekateAir".to_string()
44    }
45
46    fn num_columns(&self) -> usize {
47        self.virtual_column_layout().len()
48    }
49
50    /// Flat expansion of `constraint_ast()`.
51    fn constraints(&self) -> Vec<Constraint<F>> {
52        self.constraint_ast().to_constraints()
53    }
54
55    /// Returns the list of boundary constraints. Each
56    /// constraint ties a specific trace cell to a public
57    /// input value. By default, returns an empty list.
58    fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
59        Vec::new()
60    }
61
62    /// Returns the physical layout of the columns in the trace.
63    ///
64    /// This describes the storage type
65    /// (Bit, B8, B32, etc.) of each column.
66    fn column_layout(&self) -> &[ColumnType];
67
68    /// Returns the virtual layout of the columns
69    /// (after unpacking). Defaults to the expander's
70    /// layout if present, else the physical layout.
71    fn virtual_column_layout(&self) -> &[ColumnType] {
72        match self.virtual_expander() {
73            Some(e) => e.virtual_layout(),
74            None => self.column_layout(),
75        }
76    }
77
78    /// Returns the permutation check
79    /// specifications for this AIR table.
80    ///
81    /// Each tuple contains:
82    /// - `String`:
83    ///   Unique bus identifier (e.g., `RomChiplet::BUS_ID`)
84    /// - `PermutationCheckSpec`:
85    ///   The GPA specification (sources, selector)
86    ///
87    /// Default:
88    /// No permutation checks.
89    fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
90        Vec::new()
91    }
92
93    /// Columns pinned to a fixed shape, bound by MLE
94    /// equality at `r_final`. Each shape must be a pure
95    /// function of the row index, not the witness.
96    fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
97        Vec::new()
98    }
99
100    /// Returns the `VirtualExpander` for chiplets
101    /// with physical to virtual column expansion.
102    /// Non-reuse entries must tile `column_layout()`
103    /// exactly; an uncovered column is bound by nothing.
104    fn virtual_expander(&self) -> Option<&VirtualExpander> {
105        None
106    }
107
108    /// Parses a raw physical row (bytes) into the full
109    /// Virtual Row (fields). Used by the Verifier to
110    /// reconstruct the virtual trace from committed data.
111    ///
112    /// Delegates to `virtual_expander().parse_row()` when present.
113    /// Falls back to 1:1 parsing from `column_layout()`.
114    fn parse_virtual_row(&self, bytes: &[u8], res: &mut Vec<Flat<F>>)
115    where
116        F: TraceCompatibleField,
117    {
118        res.clear();
119
120        if let Some(e) = self.virtual_expander() {
121            e.parse_row(bytes, res)
122                .expect("committed row byte length must match physical_row_bytes");
123            return;
124        }
125
126        let mut offset = 0;
127        for col_type in self.column_layout() {
128            let size = col_type.byte_size();
129            if offset + size <= bytes.len() {
130                res.push(col_type.parse_from_bytes(&bytes[offset..offset + size]));
131                offset += size;
132            }
133        }
134    }
135
136    /// Returns the constraint system as an AST-DAG.
137    fn constraint_ast(&self) -> ConstraintAst<F>;
138
139    /// Chiplet defs used only for kernel dispatch.
140    fn inline_chiplets(&self) -> errors::Result<Vec<chiplet::ChipletDef<F>>> {
141        Ok(Vec::new())
142    }
143
144    /// Each hint's `chiplet_idx` indexes into `inline_chiplets()`.
145    fn inline_chiplet_kernels(&self) -> Vec<InlineKernelHint> {
146        Vec::new()
147    }
148}
149
150// =================================================================
151// Composition over Air
152// =================================================================
153
154/// Extends `Air<F>` with multi-table composition:
155/// independent chiplets, GKR gadgets, and public inputs.
156///
157/// The top-level prover and verifier require `Program<F>`.
158/// Internal sub-protocols (ZeroCheck, chiplet verification)
159/// operate on `Air<F>` alone.
160pub trait Program<F: TowerField>: Air<F> {
161    /// Number of public inputs for this program.
162    fn num_public_inputs(&self) -> usize {
163        0
164    }
165
166    /// Returns independent AIR chiplet definitions.
167    /// Each chiplet gets its own trace, commitment,
168    /// ZeroCheck, and evaluation argument.
169    /// Connected to the main trace via GPA bus.
170    fn chiplet_defs(&self) -> errors::Result<Vec<chiplet::ChipletDef<F>>> {
171        Ok(Vec::new())
172    }
173}
174
175/// Represents a reference to a trace cell within
176/// the program's execution trace. Points to a specific
177/// column and relative row offset (current or next).
178#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
179pub struct ProgramCell {
180    pub col_idx: usize,
181
182    /// false = current row (i),
183    /// true = next row (i+1)
184    pub next_row: bool,
185}
186
187impl ProgramCell {
188    /// Reference to a cell in the current row.
189    pub fn current(col_idx: usize) -> Self {
190        Self {
191            col_idx,
192            next_row: false,
193        }
194    }
195
196    /// Reference to a cell in the next row.
197    pub fn next(col_idx: usize) -> Self {
198        Self {
199            col_idx,
200            next_row: true,
201        }
202    }
203}
204
205// =================================================================
206// INSTANCE & WITNESS
207// =================================================================
208
209/// Public Instance (Common inputs)
210/// of the program execution.
211#[derive(Clone, Debug)]
212pub struct ProgramInstance<F: TowerField> {
213    num_rows: usize,
214    public_inputs: Vec<F>,
215}
216
217impl<F: TowerField> ProgramInstance<F> {
218    pub fn new(num_rows: usize, public_inputs: Vec<F>) -> Self {
219        assert!(
220            num_rows.is_power_of_two(),
221            "Program trace height must be power of 2"
222        );
223
224        Self {
225            num_rows,
226            public_inputs,
227        }
228    }
229
230    #[inline(always)]
231    pub fn num_rows(&self) -> usize {
232        self.num_rows
233    }
234
235    /// Public inputs in canonical basis.
236    #[inline(always)]
237    pub fn public_inputs(&self) -> &[F] {
238        &self.public_inputs
239    }
240
241    #[inline(always)]
242    pub fn public_input(&self, idx: usize) -> Option<F> {
243        self.public_inputs.get(idx).copied()
244    }
245}
246
247/// Secret Witness (The Execution Trace) of the program.
248/// Holds the trace data. Generic over T to support both
249/// raw ColumnTrace and specialized wrappers.
250pub struct ProgramWitness<F: TowerField, T: Trace = ColumnTrace> {
251    pub trace: T,
252    pub chiplet_traces: Vec<ColumnTrace>,
253    _marker: PhantomData<F>,
254}
255
256impl<F: TowerField, T: Trace> ProgramWitness<F, T> {
257    pub fn new(trace: T) -> Self {
258        Self {
259            trace,
260            chiplet_traces: Vec::new(),
261            _marker: PhantomData,
262        }
263    }
264
265    /// Attach independent chiplet traces.
266    /// Each entry corresponds by index to `chiplet_defs()`.
267    pub fn with_chiplets(mut self, chiplet_traces: Vec<ColumnTrace>) -> Self {
268        self.chiplet_traces = chiplet_traces;
269        self
270    }
271}
272
273/// Locates a chiplet's inlined sub-AST in
274/// the program's merged `constraint_ast()`
275/// so the prover can dispatch its kernel.
276#[derive(Clone, Copy, Debug)]
277pub struct InlineKernelHint {
278    /// Index into `Air::inline_chiplets()`.
279    pub chiplet_idx: usize,
280
281    /// Absolute index of the chiplet's
282    /// first root in the program's `roots`.
283    pub root_offset: usize,
284
285    /// Absolute column index where
286    /// the chiplet's columns start.
287    pub column_offset: usize,
288}
289
290// =================================================================
291// FIXED COLUMNS
292// =================================================================
293
294#[derive(Clone, Debug, PartialEq, Eq)]
295pub struct CadenceSegment<F> {
296    pub stride: usize,
297    pub count: usize,
298    pub origin: usize,
299    pub values: Vec<F>,
300}
301
302/// Row-index-determined shape a fixed column is pinned to.
303/// `LastRow` is one on every row but the last.
304#[derive(Clone, Debug, PartialEq, Eq)]
305pub enum FixedShape<F> {
306    LastRow,
307    FirstRow,
308    Custom(Vec<bool>),
309    Periodic {
310        period: usize,
311        values: Vec<F>,
312    },
313    Sparse(Vec<(usize, F)>),
314    Dense(Vec<F>),
315
316    /// `values[(row - origin) % stride]` on rows
317    /// `[origin, origin + stride·count)`, zero outside.
318    Cadence {
319        stride: usize,
320        count: usize,
321        origin: usize,
322        values: Vec<F>,
323    },
324
325    /// Disjoint union of cadence blocks, sorted by
326    /// origin; the MLE is the sum of the block MLEs.
327    Segments(Vec<CadenceSegment<F>>),
328}
329
330impl<F> FixedShape<F> {
331    /// Whether the shape keeps the committed column readable.
332    /// The rest are supplied as indicators, never read back.
333    pub fn is_overlay(&self) -> bool {
334        match self {
335            Self::Periodic { .. }
336            | Self::Sparse(..)
337            | Self::Dense(..)
338            | Self::Cadence { .. }
339            | Self::Segments(..) => true,
340            Self::LastRow | Self::FirstRow | Self::Custom(..) => false,
341        }
342    }
343}
344
345impl<F: HardwareField> FixedShape<F> {
346    /// MLE of the shape at point `r` (LSB-first),
347    /// in flat basis. `r.len()` must equal `num_vars`.
348    pub fn evaluate(&self, r: &[Flat<F>]) -> Flat<F> {
349        let one = Flat::from_raw(F::ONE);
350        match self {
351            FixedShape::LastRow => {
352                let mut prod = one;
353                for &r_k in r {
354                    prod *= r_k;
355                }
356
357                one - prod
358            }
359            FixedShape::FirstRow => {
360                let mut prod = one;
361                for &r_k in r {
362                    prod *= one - r_k;
363                }
364
365                prod
366            }
367            FixedShape::Custom(bits) => {
368                debug_assert_eq!(bits.len(), r.len(), "Custom point bit width != r.len()");
369
370                let mut prod = one;
371                for (k, &b) in bits.iter().enumerate() {
372                    let factor = if b { r[k] } else { one - r[k] };
373                    prod *= factor;
374                }
375
376                prod
377            }
378            FixedShape::Periodic { period, values } => {
379                // Low p = log2(period) coords only;
380                // high coords each sum to 1.
381                let p = period.trailing_zeros() as usize;
382
383                let mut acc = Flat::from_raw(F::ZERO);
384                for (j, &v) in values.iter().enumerate() {
385                    acc += v.to_hardware() * eq_index(&r[..p], j);
386                }
387
388                acc
389            }
390            FixedShape::Sparse(entries) => {
391                let mut acc = Flat::from_raw(F::ZERO);
392                for &(row, v) in entries {
393                    acc += v.to_hardware() * eq_index(r, row);
394                }
395
396                acc
397            }
398            FixedShape::Dense(values) => {
399                let mut acc = Flat::from_raw(F::ZERO);
400                for (i, &v) in values.iter().enumerate() {
401                    acc += v.to_hardware() * eq_index(r, i);
402                }
403
404                acc
405            }
406            FixedShape::Cadence {
407                stride,
408                count,
409                origin,
410                values,
411            } => cadence_mle(r, *stride, *count, *origin, values),
412            FixedShape::Segments(segments) => {
413                let mut acc = Flat::from_raw(F::ZERO);
414                for seg in segments {
415                    acc += cadence_mle(r, seg.stride, seg.count, seg.origin, &seg.values);
416                }
417
418                acc
419            }
420        }
421    }
422
423    /// Shape value at integer row `row`. O(1);
424    /// prefer over `evaluate` at a vertex,
425    /// which is O(N) for `Dense`.
426    pub fn value_at_row(&self, row: usize, num_vars: usize) -> Flat<F> {
427        let one = Flat::from_raw(F::ONE);
428        let zero = Flat::from_raw(F::ZERO);
429
430        match self {
431            FixedShape::FirstRow => {
432                if row == 0 {
433                    one
434                } else {
435                    zero
436                }
437            }
438            FixedShape::LastRow => {
439                if row == (1usize << num_vars) - 1 {
440                    zero
441                } else {
442                    one
443                }
444            }
445            FixedShape::Custom(bits) => {
446                let target = bits
447                    .iter()
448                    .enumerate()
449                    .fold(0usize, |acc, (k, &b)| acc | ((b as usize) << k));
450
451                if row == target { one } else { zero }
452            }
453            FixedShape::Periodic { period, values } => values[row % period].to_hardware(),
454            FixedShape::Sparse(entries) => {
455                let mut acc = zero;
456                for &(r, v) in entries {
457                    if r == row {
458                        acc += v.to_hardware();
459                    }
460                }
461
462                acc
463            }
464            FixedShape::Dense(values) => values[row].to_hardware(),
465            FixedShape::Cadence {
466                stride,
467                count,
468                origin,
469                values,
470            } => cadence_value_at(row, *stride, *count, *origin, values),
471            FixedShape::Segments(segments) => {
472                let mut acc = zero;
473                for seg in segments {
474                    acc += cadence_value_at(row, seg.stride, seg.count, seg.origin, &seg.values);
475                }
476
477                acc
478            }
479        }
480    }
481}
482
483/// One committed column pinned to a fixed shape.
484#[derive(Clone, Debug, PartialEq, Eq)]
485pub struct FixedColumn<F> {
486    pub col_idx: usize,
487    pub shape: FixedShape<F>,
488}
489
490impl<F> FixedColumn<F> {
491    pub fn last_row(col_idx: usize) -> Self {
492        Self {
493            col_idx,
494            shape: FixedShape::LastRow,
495        }
496    }
497
498    pub fn first_row(col_idx: usize) -> Self {
499        Self {
500            col_idx,
501            shape: FixedShape::FirstRow,
502        }
503    }
504
505    pub fn prefix(col_idx: usize, count: usize) -> Self
506    where
507        F: TowerField,
508    {
509        Self {
510            col_idx,
511            shape: FixedShape::Cadence {
512                stride: 1,
513                count,
514                origin: 0,
515                values: vec![F::ONE],
516            },
517        }
518    }
519
520    pub fn custom(col_idx: usize, bits: Vec<bool>) -> Self {
521        Self {
522            col_idx,
523            shape: FixedShape::Custom(bits),
524        }
525    }
526
527    pub fn periodic(col_idx: usize, period: usize, values: Vec<F>) -> Self {
528        Self {
529            col_idx,
530            shape: FixedShape::Periodic { period, values },
531        }
532    }
533
534    pub fn sparse(col_idx: usize, entries: Vec<(usize, F)>) -> Self {
535        Self {
536            col_idx,
537            shape: FixedShape::Sparse(entries),
538        }
539    }
540
541    pub fn dense(col_idx: usize, values: Vec<F>) -> Self {
542        Self {
543            col_idx,
544            shape: FixedShape::Dense(values),
545        }
546    }
547
548    pub fn segments(col_idx: usize, segments: Vec<CadenceSegment<F>>) -> Self {
549        Self {
550            col_idx,
551            shape: FixedShape::Segments(segments),
552        }
553    }
554}
555
556/// Declares a fixed column from a shape.
557pub fn fix<F>(col_idx: usize, shape: FixedShape<F>) -> FixedColumn<F> {
558    FixedColumn { col_idx, shape }
559}
560
561/// Rejects out-of-range `col_idx`, duplicate pins,
562/// malformed shapes, and out-of-domain values
563/// (`Bit` columns require values in {0, 1}).
564pub fn validate_fixed_columns<F: TowerField>(
565    fixed: &[FixedColumn<F>],
566    layout: &[ColumnType],
567    num_vars: Option<usize>,
568) -> errors::Result<()> {
569    for (i, fc) in fixed.iter().enumerate() {
570        if fc.col_idx >= layout.len() {
571            return Err(errors::Error::Protocol {
572                protocol: "fixed_column",
573                message: "col_idx out of range",
574            });
575        }
576
577        validate_shape(&fc.shape, layout[fc.col_idx], num_vars)?;
578
579        for prior in &fixed[..i] {
580            if prior.col_idx == fc.col_idx {
581                return Err(errors::Error::Protocol {
582                    protocol: "fixed_column",
583                    message: "duplicate pin on same column",
584                });
585            }
586        }
587    }
588
589    Ok(())
590}
591
592/// `Σ_{i < limit, i ≡ c (mod stride)} eq(r, i)` per residue class `c`.
593fn prefix_residue_sums<F: HardwareField>(
594    r: &[Flat<F>],
595    limit: usize,
596    stride: usize,
597) -> Vec<Flat<F>> {
598    let one = Flat::from_raw(F::ONE);
599    let zero = Flat::from_raw(F::ZERO);
600    let nv = r.len();
601
602    let full = match 1usize.checked_shl(nv as u32) {
603        Some(cap) => limit >= cap,
604        None => false,
605    };
606
607    let mut pow2_mod = Vec::with_capacity(nv);
608    let mut p2 = 1usize % stride;
609
610    for _ in 0..nv {
611        pow2_mod.push(p2);
612        p2 = (p2 * 2) % stride;
613    }
614
615    let mut free = alloc::vec![zero; stride];
616    let mut next_free = alloc::vec![zero; stride];
617    let mut tight = if full { zero } else { one };
618    let mut tight_residue = 0usize;
619
620    if full {
621        free[0] = one;
622    }
623
624    for k in (0..nv).rev() {
625        let p2k = pow2_mod[k];
626        for slot in next_free.iter_mut() {
627            *slot = zero;
628        }
629
630        for c in 0..stride {
631            let w = free[c];
632
633            next_free[c] += w * (one - r[k]);
634            next_free[(c + p2k) % stride] += w * r[k];
635        }
636
637        if (limit >> k) & 1 == 1 {
638            next_free[tight_residue] += tight * (one - r[k]);
639            tight *= r[k];
640            tight_residue = (tight_residue + p2k) % stride;
641        } else {
642            tight *= one - r[k];
643        }
644
645        core::mem::swap(&mut free, &mut next_free);
646    }
647
648    free
649}
650
651fn cadence_mle<F: HardwareField>(
652    r: &[Flat<F>],
653    stride: usize,
654    count: usize,
655    origin: usize,
656    values: &[F],
657) -> Flat<F> {
658    if stride == 0 || values.len() != stride {
659        return Flat::from_raw(F::ZERO);
660    }
661
662    let end = origin.saturating_add(stride.saturating_mul(count));
663
664    let mut class_sums = prefix_residue_sums(r, end, stride);
665    let start_sums = prefix_residue_sums(r, origin, stride);
666
667    for (c, s) in start_sums.iter().enumerate() {
668        class_sums[c] += *s;
669    }
670
671    let mut acc = Flat::from_raw(F::ZERO);
672    for (c, sum) in class_sums.iter().enumerate() {
673        let offset = (c + stride - origin % stride) % stride;
674        acc += values[offset].to_hardware() * *sum;
675    }
676
677    acc
678}
679
680fn cadence_value_at<F: HardwareField>(
681    row: usize,
682    stride: usize,
683    count: usize,
684    origin: usize,
685    values: &[F],
686) -> Flat<F> {
687    if stride == 0 || values.len() != stride {
688        return Flat::from_raw(F::ZERO);
689    }
690
691    let end = origin.saturating_add(stride.saturating_mul(count));
692
693    if row < origin || row >= end {
694        Flat::from_raw(F::ZERO)
695    } else {
696        values[(row - origin) % stride].to_hardware()
697    }
698}
699
700fn eq_index<F: HardwareField>(r: &[Flat<F>], index: usize) -> Flat<F> {
701    let one = Flat::from_raw(F::ONE);
702    let mut prod = one;
703
704    for (k, &r_k) in r.iter().enumerate() {
705        let factor = if (index >> k) & 1 == 1 {
706            r_k
707        } else {
708            one - r_k
709        };
710
711        prod *= factor;
712    }
713
714    prod
715}
716
717fn validate_shape<F: TowerField>(
718    shape: &FixedShape<F>,
719    col_type: ColumnType,
720    num_vars: Option<usize>,
721) -> errors::Result<()> {
722    match shape {
723        FixedShape::LastRow | FixedShape::FirstRow => Ok(()),
724        FixedShape::Custom(bits) => match num_vars {
725            Some(nv) if bits.len() != nv => Err(errors::Error::Protocol {
726                protocol: "fixed_column",
727                message: "Custom point bit width != num_vars",
728            }),
729            _ => Ok(()),
730        },
731        FixedShape::Periodic { period, values } => {
732            if !period.is_power_of_two() {
733                return Err(errors::Error::Protocol {
734                    protocol: "fixed_column",
735                    message: "Periodic period must be a power of two",
736                });
737            }
738
739            if values.len() != *period {
740                return Err(errors::Error::Protocol {
741                    protocol: "fixed_column",
742                    message: "Periodic values length != period",
743                });
744            }
745
746            if let Some(nv) = num_vars
747                && *period > (1usize << nv)
748            {
749                return Err(errors::Error::Protocol {
750                    protocol: "fixed_column",
751                    message: "Periodic period exceeds trace height",
752                });
753            }
754
755            check_bit_domain(values.iter().copied(), col_type)
756        }
757        FixedShape::Sparse(entries) => {
758            if let Some(nv) = num_vars {
759                let n = 1usize << nv;
760                for &(row, _) in entries {
761                    if row >= n {
762                        return Err(errors::Error::Protocol {
763                            protocol: "fixed_column",
764                            message: "Sparse row index exceeds trace height",
765                        });
766                    }
767                }
768            }
769
770            for (i, &(row, _)) in entries.iter().enumerate() {
771                if entries[..i].iter().any(|&(prior, _)| prior == row) {
772                    return Err(errors::Error::Protocol {
773                        protocol: "fixed_column",
774                        message: "duplicate Sparse row",
775                    });
776                }
777            }
778
779            check_bit_domain(entries.iter().map(|&(_, v)| v), col_type)
780        }
781        FixedShape::Dense(values) => {
782            if let Some(nv) = num_vars
783                && values.len() != (1usize << nv)
784            {
785                return Err(errors::Error::Protocol {
786                    protocol: "fixed_column",
787                    message: "Dense values length != trace height",
788                });
789            }
790
791            check_bit_domain(values.iter().copied(), col_type)
792        }
793        FixedShape::Cadence {
794            stride,
795            count,
796            origin,
797            values,
798        } => {
799            if *stride == 0 {
800                return Err(errors::Error::Protocol {
801                    protocol: "fixed_column",
802                    message: "Cadence stride must be non-zero",
803                });
804            }
805
806            if values.len() != *stride {
807                return Err(errors::Error::Protocol {
808                    protocol: "fixed_column",
809                    message: "Cadence values length != stride",
810                });
811            }
812
813            let end = stride
814                .checked_mul(*count)
815                .and_then(|span| span.checked_add(*origin))
816                .ok_or(errors::Error::Protocol {
817                    protocol: "fixed_column",
818                    message: "Cadence active range overflows",
819                })?;
820
821            if let Some(nv) = num_vars
822                && end > (1usize << nv)
823            {
824                return Err(errors::Error::Protocol {
825                    protocol: "fixed_column",
826                    message: "Cadence active range exceeds trace height",
827                });
828            }
829
830            check_bit_domain(values.iter().copied(), col_type)
831        }
832        FixedShape::Segments(segments) => {
833            if segments.is_empty() {
834                return Err(errors::Error::Protocol {
835                    protocol: "fixed_column",
836                    message: "Segments list is empty",
837                });
838            }
839
840            let mut prev_end = 0usize;
841            for seg in segments {
842                if seg.stride == 0 {
843                    return Err(errors::Error::Protocol {
844                        protocol: "fixed_column",
845                        message: "Segments stride must be non-zero",
846                    });
847                }
848
849                if seg.count == 0 {
850                    return Err(errors::Error::Protocol {
851                        protocol: "fixed_column",
852                        message: "Segments count must be non-zero",
853                    });
854                }
855
856                if seg.values.len() != seg.stride {
857                    return Err(errors::Error::Protocol {
858                        protocol: "fixed_column",
859                        message: "Segments values length != stride",
860                    });
861                }
862
863                let end = seg
864                    .stride
865                    .checked_mul(seg.count)
866                    .and_then(|span| span.checked_add(seg.origin))
867                    .ok_or(errors::Error::Protocol {
868                        protocol: "fixed_column",
869                        message: "Segments active range overflows",
870                    })?;
871
872                if seg.origin < prev_end {
873                    return Err(errors::Error::Protocol {
874                        protocol: "fixed_column",
875                        message: "Segments must be sorted by origin and disjoint",
876                    });
877                }
878
879                prev_end = end;
880
881                check_bit_domain(seg.values.iter().copied(), col_type)?;
882            }
883
884            if let Some(nv) = num_vars
885                && prev_end > (1usize << nv)
886            {
887                return Err(errors::Error::Protocol {
888                    protocol: "fixed_column",
889                    message: "Segments active range exceeds trace height",
890                });
891            }
892
893            Ok(())
894        }
895    }
896}
897
898fn check_bit_domain<F: TowerField>(
899    values: impl Iterator<Item = F>,
900    col_type: ColumnType,
901) -> errors::Result<()> {
902    if col_type != ColumnType::Bit {
903        return Ok(());
904    }
905
906    for v in values {
907        if v != F::ZERO && v != F::ONE {
908            return Err(errors::Error::Protocol {
909                protocol: "fixed_column",
910                message: "Bit fixed column value not in {0,1}",
911            });
912        }
913    }
914
915    Ok(())
916}