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//! The circuit DSL of Hekate, the Rust zero-knowledge proof engine.
6//!
7//! A [`circuit::Circuit`] declares a table's columns with [`define_columns!`],
8//! relates its rows with constraints, pins cells and fixed columns,
9//! publishes public inputs and calls chiplets over LogUp buses.
10//! It compiles into a [`Program`]: the prover runs it on a [`ProgramWitness`],
11//! and the verifier checks it against a [`ProgramInstance`].
12//! [`digest::program_id`] hashes the program for the verifier to pin.
13//!
14//! - [Your First ZK Program][first]
15//! - [AIR Constraints][air]
16//! - [LogUp Buses][logup]
17//! - [Cryptographic Chiplets][chiplets]
18//!
19//! [first]: https://oumuamua.dev/hekate/docs/getting-started/your-first-zk-program
20//! [air]: https://oumuamua.dev/hekate/docs/basics/air-constraints
21//! [logup]: https://oumuamua.dev/hekate/docs/basics/logup-buses
22//! [chiplets]: https://oumuamua.dev/hekate/docs/basics/cryptographic-chiplets
23
24#![cfg_attr(not(feature = "std"), no_std)]
25
26extern crate alloc;
27extern crate core;
28
29use alloc::collections::BTreeMap;
30use alloc::string::{String, ToString};
31use alloc::vec;
32use alloc::vec::Vec;
33use constraint::{BoundaryConstraint, Constraint, ConstraintAst};
34use core::marker::PhantomData;
35use expander::VirtualExpander;
36use hekate_core::errors;
37use hekate_core::trace::{ColumnTrace, ColumnType, Trace, TraceCompatibleField};
38use hekate_math::{Flat, HardwareField, TowerField};
39use permutation::PermutationCheckSpec;
40
41pub mod chiplet;
42pub mod circuit;
43pub mod constraint;
44pub mod digest;
45pub mod expander;
46pub mod linearized;
47pub mod outer;
48pub mod permutation;
49pub mod predicate;
50pub mod schema;
51
52// =================================================================
53// Core Algebraic Intermediate Representation
54// =================================================================
55
56/// Defines the algebraic structure, trace
57/// layout, and constraints of an AIR table.
58///
59/// Implemented by both standalone
60/// programs and independent chiplets.
61pub trait Air<F: TowerField>: Sized + Clone + Sync {
62    fn name(&self) -> String {
63        "HekateAir".to_string()
64    }
65
66    fn num_columns(&self) -> usize {
67        self.virtual_column_layout().len()
68    }
69
70    /// Flat expansion of `constraint_ast()`.
71    fn constraints(&self) -> Vec<Constraint<F>> {
72        self.constraint_ast().to_constraints()
73    }
74
75    /// Returns the list of boundary constraints. Each
76    /// constraint ties a specific trace cell to a public
77    /// input value. By default, returns an empty list.
78    fn boundary_constraints(&self) -> Vec<BoundaryConstraint<F>> {
79        Vec::new()
80    }
81
82    /// Returns the physical layout of the columns in the trace.
83    ///
84    /// This describes the storage type
85    /// (Bit, B8, B32, etc.) of each column.
86    fn column_layout(&self) -> &[ColumnType];
87
88    /// Returns the virtual layout of the columns
89    /// (after unpacking). Defaults to the expander's
90    /// layout if present, else the physical layout.
91    fn virtual_column_layout(&self) -> &[ColumnType] {
92        match self.virtual_expander() {
93            Some(e) => e.virtual_layout(),
94            None => self.column_layout(),
95        }
96    }
97
98    /// Returns the permutation check
99    /// specifications for this AIR table.
100    ///
101    /// Each tuple contains:
102    /// - `String`:
103    ///   Unique bus identifier (e.g., `RomChiplet::BUS_ID`)
104    /// - `PermutationCheckSpec`:
105    ///   The GPA specification (sources, selector)
106    ///
107    /// Default:
108    /// No permutation checks.
109    fn permutation_checks(&self) -> Vec<(String, PermutationCheckSpec)> {
110        Vec::new()
111    }
112
113    /// Columns pinned to a fixed shape, bound by MLE
114    /// equality at `r_final`. Each shape must be a pure
115    /// function of the row index, not the witness.
116    fn fixed_columns(&self) -> Vec<FixedColumn<F>> {
117        Vec::new()
118    }
119
120    /// Returns the `VirtualExpander` for chiplets
121    /// with physical to virtual column expansion.
122    /// Non-reuse entries must tile `column_layout()`
123    /// exactly; an uncovered column is bound by nothing.
124    fn virtual_expander(&self) -> Option<&VirtualExpander> {
125        None
126    }
127
128    /// Parses a raw physical row (bytes) into the full
129    /// Virtual Row (fields). Used by the Verifier to
130    /// reconstruct the virtual trace from committed data.
131    ///
132    /// Delegates to `virtual_expander().parse_row()` when present.
133    /// Falls back to 1:1 parsing from `column_layout()`.
134    fn parse_virtual_row(&self, bytes: &[u8], res: &mut Vec<Flat<F>>)
135    where
136        F: TraceCompatibleField,
137    {
138        res.clear();
139
140        if let Some(e) = self.virtual_expander() {
141            e.parse_row(bytes, res)
142                .expect("committed row byte length must match physical_row_bytes");
143            return;
144        }
145
146        let mut offset = 0;
147        for col_type in self.column_layout() {
148            let size = col_type.byte_size();
149            if offset + size <= bytes.len() {
150                res.push(col_type.parse_from_bytes(&bytes[offset..offset + size]));
151                offset += size;
152            }
153        }
154    }
155
156    /// Returns the constraint system as an AST-DAG.
157    fn constraint_ast(&self) -> ConstraintAst<F>;
158
159    /// Chiplet defs used only for kernel dispatch.
160    fn inline_chiplets(&self) -> errors::Result<Vec<chiplet::ChipletDef<F>>> {
161        Ok(Vec::new())
162    }
163
164    /// Each hint's `chiplet_idx` indexes into `inline_chiplets()`.
165    fn inline_chiplet_kernels(&self) -> Vec<InlineKernelHint> {
166        Vec::new()
167    }
168}
169
170// =================================================================
171// Composition over Air
172// =================================================================
173
174/// Extends `Air<F>` with multi-table composition:
175/// independent chiplets, GKR gadgets, and public inputs.
176///
177/// The top-level prover and verifier require `Program<F>`.
178/// Internal sub-protocols (ZeroCheck, chiplet verification)
179/// operate on `Air<F>` alone.
180pub trait Program<F: TowerField>: Air<F> {
181    /// Number of public inputs for this program.
182    fn num_public_inputs(&self) -> usize {
183        0
184    }
185
186    /// Returns independent AIR chiplet definitions.
187    /// Each chiplet gets its own trace, commitment,
188    /// ZeroCheck, and evaluation argument.
189    /// Connected to the main trace via GPA bus.
190    fn chiplet_defs(&self) -> errors::Result<Vec<chiplet::ChipletDef<F>>> {
191        Ok(Vec::new())
192    }
193}
194
195/// Represents a reference to a trace cell within
196/// the program's execution trace. Points to a specific
197/// column and relative row offset (current or next).
198#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
199pub struct ProgramCell {
200    pub col_idx: usize,
201
202    /// false = current row (i),
203    /// true = next row (i+1)
204    pub next_row: bool,
205}
206
207impl ProgramCell {
208    /// Reference to a cell in the current row.
209    pub fn current(col_idx: usize) -> Self {
210        Self {
211            col_idx,
212            next_row: false,
213        }
214    }
215
216    /// Reference to a cell in the next row.
217    pub fn next(col_idx: usize) -> Self {
218        Self {
219            col_idx,
220            next_row: true,
221        }
222    }
223}
224
225// =================================================================
226// INSTANCE & WITNESS
227// =================================================================
228
229/// Public Instance (Common inputs)
230/// of the program execution.
231#[derive(Clone, Debug)]
232pub struct ProgramInstance<F: TowerField> {
233    num_rows: usize,
234    public_inputs: Vec<F>,
235}
236
237impl<F: TowerField> ProgramInstance<F> {
238    pub fn new(num_rows: usize, public_inputs: Vec<F>) -> Self {
239        assert!(
240            num_rows.is_power_of_two(),
241            "Program trace height must be power of 2"
242        );
243
244        Self {
245            num_rows,
246            public_inputs,
247        }
248    }
249
250    #[inline(always)]
251    pub fn num_rows(&self) -> usize {
252        self.num_rows
253    }
254
255    /// Public inputs in canonical basis.
256    #[inline(always)]
257    pub fn public_inputs(&self) -> &[F] {
258        &self.public_inputs
259    }
260
261    #[inline(always)]
262    pub fn public_input(&self, idx: usize) -> Option<F> {
263        self.public_inputs.get(idx).copied()
264    }
265}
266
267/// Secret Witness (The Execution Trace) of the program.
268/// Holds the trace data. Generic over T to support both
269/// raw ColumnTrace and specialized wrappers.
270pub struct ProgramWitness<F: TowerField, T: Trace = ColumnTrace> {
271    pub trace: T,
272    pub chiplet_traces: Vec<ColumnTrace>,
273    _marker: PhantomData<F>,
274}
275
276impl<F: TowerField, T: Trace> ProgramWitness<F, T> {
277    pub fn new(trace: T) -> Self {
278        Self {
279            trace,
280            chiplet_traces: Vec::new(),
281            _marker: PhantomData,
282        }
283    }
284
285    /// Attach independent chiplet traces.
286    /// Each entry corresponds by index to `chiplet_defs()`.
287    pub fn with_chiplets(mut self, chiplet_traces: Vec<ColumnTrace>) -> Self {
288        self.chiplet_traces = chiplet_traces;
289        self
290    }
291}
292
293/// Locates a chiplet's inlined sub-AST in
294/// the program's merged `constraint_ast()`
295/// so the prover can dispatch its kernel.
296#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297pub struct InlineKernelHint {
298    /// Index into `Air::inline_chiplets()`.
299    pub chiplet_idx: usize,
300
301    /// Absolute index of the chiplet's
302    /// first root in the program's `roots`.
303    pub root_offset: usize,
304
305    /// Absolute column index where
306    /// the chiplet's columns start.
307    pub column_offset: usize,
308}
309
310// =================================================================
311// FIXED COLUMNS
312// =================================================================
313
314/// Most variables [`ShapeEvaluator`] builds eq tables for:
315/// a proof picks a chiplet's height, and the larger
316/// table holds `2^⌈num_vars / 2⌉` cells.
317const EQ_TABLE_MAX_VARS: usize = 32;
318
319#[derive(Clone, Debug, PartialEq, Eq)]
320pub struct CadenceSegment<F> {
321    pub stride: usize,
322    pub count: usize,
323    pub origin: usize,
324    pub values: Vec<F>,
325}
326
327/// Row-index-determined shape a fixed column is pinned to.
328/// `LastRow` is one on every row but the last.
329#[derive(Clone, Debug, PartialEq, Eq)]
330pub enum FixedShape<F> {
331    LastRow,
332    FirstRow,
333    Custom(Vec<bool>),
334    Periodic {
335        period: usize,
336        values: Vec<F>,
337    },
338    Sparse(Vec<(usize, F)>),
339    Dense(Vec<F>),
340
341    /// `values[(row - origin) % stride]` on rows
342    /// `[origin, origin + stride·count)`, zero outside.
343    Cadence {
344        stride: usize,
345        count: usize,
346        origin: usize,
347        values: Vec<F>,
348    },
349
350    /// Disjoint union of cadence blocks, sorted by
351    /// origin; the MLE is the sum of the block MLEs.
352    Segments(Vec<CadenceSegment<F>>),
353}
354
355impl<F> FixedShape<F> {
356    /// Whether the shape keeps the committed column readable.
357    /// The rest are supplied as indicators, never read back.
358    pub fn is_overlay(&self) -> bool {
359        match self {
360            Self::Periodic { .. }
361            | Self::Sparse(..)
362            | Self::Dense(..)
363            | Self::Cadence { .. }
364            | Self::Segments(..) => true,
365            Self::LastRow | Self::FirstRow | Self::Custom(..) => false,
366        }
367    }
368}
369
370impl<F: HardwareField> FixedShape<F> {
371    /// MLE of the shape at point `r` (LSB-first),
372    /// in flat basis. `r.len()` must equal `num_vars`.
373    pub fn evaluate(&self, r: &[Flat<F>]) -> Flat<F> {
374        let one = Flat::from_raw(F::ONE);
375        match self {
376            FixedShape::LastRow => {
377                let mut prod = one;
378                for &r_k in r {
379                    prod *= r_k;
380                }
381
382                one - prod
383            }
384            FixedShape::FirstRow => {
385                let mut prod = one;
386                for &r_k in r {
387                    prod *= one - r_k;
388                }
389
390                prod
391            }
392            FixedShape::Custom(bits) => {
393                debug_assert_eq!(bits.len(), r.len(), "Custom point bit width != r.len()");
394
395                let mut prod = one;
396                for (k, &b) in bits.iter().enumerate() {
397                    let factor = if b { r[k] } else { one - r[k] };
398                    prod *= factor;
399                }
400
401                prod
402            }
403            FixedShape::Periodic { period, values } => {
404                // Low p = log2(period) coords only;
405                // high coords each sum to 1.
406                let p = period.trailing_zeros() as usize;
407
408                let mut acc = Flat::from_raw(F::ZERO);
409                for (j, &v) in values.iter().enumerate() {
410                    acc += v.to_hardware() * eq_index(&r[..p], j);
411                }
412
413                acc
414            }
415            FixedShape::Sparse(entries) => {
416                let mut acc = Flat::from_raw(F::ZERO);
417                for &(row, v) in entries {
418                    acc += v.to_hardware() * eq_index(r, row);
419                }
420
421                acc
422            }
423            FixedShape::Dense(values) => {
424                let mut acc = Flat::from_raw(F::ZERO);
425                for (i, &v) in values.iter().enumerate() {
426                    acc += v.to_hardware() * eq_index(r, i);
427                }
428
429                acc
430            }
431            FixedShape::Cadence {
432                stride,
433                count,
434                origin,
435                values,
436            } => cadence_mle(r, *stride, *count, *origin, values),
437            FixedShape::Segments(segments) => {
438                let mut acc = Flat::from_raw(F::ZERO);
439                for seg in segments {
440                    acc += cadence_mle(r, seg.stride, seg.count, seg.origin, &seg.values);
441                }
442
443                acc
444            }
445        }
446    }
447
448    /// Shape value at integer row `row`. O(1); prefer over `evaluate`
449    /// at a vertex, which is O(N) for `Dense`. `Segments` unvalidated
450    /// by `validate_fixed_columns` diverge from `evaluate`.
451    pub fn value_at_row(&self, row: usize, num_vars: usize) -> Flat<F> {
452        let one = Flat::from_raw(F::ONE);
453        let zero = Flat::from_raw(F::ZERO);
454
455        match self {
456            FixedShape::FirstRow => {
457                if row == 0 {
458                    one
459                } else {
460                    zero
461                }
462            }
463            FixedShape::LastRow => {
464                if row == (1usize << num_vars) - 1 {
465                    zero
466                } else {
467                    one
468                }
469            }
470            FixedShape::Custom(bits) => {
471                let target = bits
472                    .iter()
473                    .enumerate()
474                    .fold(0usize, |acc, (k, &b)| acc | ((b as usize) << k));
475
476                if row == target { one } else { zero }
477            }
478            FixedShape::Periodic { period, values } => values[row % period].to_hardware(),
479            FixedShape::Sparse(entries) => {
480                let mut acc = zero;
481                for &(r, v) in entries {
482                    if r == row {
483                        acc += v.to_hardware();
484                    }
485                }
486
487                acc
488            }
489            FixedShape::Dense(values) => values[row].to_hardware(),
490            FixedShape::Cadence {
491                stride,
492                count,
493                origin,
494                values,
495            } => cadence_value_at(row, *stride, *count, *origin, values),
496            FixedShape::Segments(segments) => {
497                // validate_shape rejects unsorted or overlapping spans:
498                // at most one covers a row.
499                let at = segments.partition_point(|seg| seg.origin <= row);
500
501                match at.checked_sub(1) {
502                    Some(i) => {
503                        let seg = &segments[i];
504
505                        cadence_value_at(row, seg.stride, seg.count, seg.origin, &seg.values)
506                    }
507                    None => zero,
508                }
509            }
510        }
511    }
512}
513
514/// One committed column pinned to a fixed shape.
515#[derive(Clone, Debug, PartialEq, Eq)]
516pub struct FixedColumn<F> {
517    pub col_idx: usize,
518    pub shape: FixedShape<F>,
519}
520
521impl<F> FixedColumn<F> {
522    pub fn last_row(col_idx: usize) -> Self {
523        Self {
524            col_idx,
525            shape: FixedShape::LastRow,
526        }
527    }
528
529    pub fn first_row(col_idx: usize) -> Self {
530        Self {
531            col_idx,
532            shape: FixedShape::FirstRow,
533        }
534    }
535
536    pub fn prefix(col_idx: usize, count: usize) -> Self
537    where
538        F: TowerField,
539    {
540        Self {
541            col_idx,
542            shape: FixedShape::Cadence {
543                stride: 1,
544                count,
545                origin: 0,
546                values: vec![F::ONE],
547            },
548        }
549    }
550
551    pub fn custom(col_idx: usize, bits: Vec<bool>) -> Self {
552        Self {
553            col_idx,
554            shape: FixedShape::Custom(bits),
555        }
556    }
557
558    pub fn periodic(col_idx: usize, period: usize, values: Vec<F>) -> Self {
559        Self {
560            col_idx,
561            shape: FixedShape::Periodic { period, values },
562        }
563    }
564
565    pub fn sparse(col_idx: usize, entries: Vec<(usize, F)>) -> Self {
566        Self {
567            col_idx,
568            shape: FixedShape::Sparse(entries),
569        }
570    }
571
572    pub fn dense(col_idx: usize, values: Vec<F>) -> Self {
573        Self {
574            col_idx,
575            shape: FixedShape::Dense(values),
576        }
577    }
578
579    pub fn segments(col_idx: usize, segments: Vec<CadenceSegment<F>>) -> Self {
580        Self {
581            col_idx,
582            shape: FixedShape::Segments(segments),
583        }
584    }
585}
586
587/// One table's fixed shapes at one point, sharing
588/// residue sums and eq tables across the shapes.
589pub struct ShapeEvaluator<'a, F> {
590    r: &'a [Flat<F>],
591    eq: Option<EqTables<F>>,
592    sums: BTreeMap<(usize, usize), Vec<Flat<F>>>,
593}
594
595impl<'a, F: HardwareField> ShapeEvaluator<'a, F> {
596    pub fn new(r: &'a [Flat<F>]) -> Self {
597        Self {
598            r,
599            eq: (r.len() <= EQ_TABLE_MAX_VARS).then(|| EqTables::new(r)),
600            sums: BTreeMap::new(),
601        }
602    }
603
604    /// MLE of `shape` at the point; equals
605    /// [`FixedShape::evaluate`] on every input.
606    pub fn evaluate(&mut self, shape: &FixedShape<F>) -> Flat<F> {
607        let zero = Flat::from_raw(F::ZERO);
608
609        match shape {
610            FixedShape::Sparse(entries) => {
611                let mut acc = zero;
612                for &(row, value) in entries {
613                    acc += scaled(value, self.eq(row));
614                }
615
616                acc
617            }
618            FixedShape::Dense(values) => {
619                let mut acc = zero;
620                for (row, &value) in values.iter().enumerate() {
621                    acc += scaled(value, self.eq(row));
622                }
623
624                acc
625            }
626            FixedShape::Cadence {
627                stride,
628                count,
629                origin,
630                values,
631            } => self.cadence(*stride, *count, *origin, values),
632            FixedShape::Segments(segments) => {
633                let mut acc = zero;
634                for seg in segments {
635                    acc += self.cadence(seg.stride, seg.count, seg.origin, &seg.values);
636                }
637
638                acc
639            }
640            FixedShape::LastRow
641            | FixedShape::FirstRow
642            | FixedShape::Custom(_)
643            | FixedShape::Periodic { .. } => shape.evaluate(self.r),
644        }
645    }
646
647    /// `eq(r, row)`, ignoring the bits of
648    /// `row` past `r.len()` as `eq_index` does.
649    fn eq(&self, row: usize) -> Flat<F> {
650        match &self.eq {
651            Some(tables) => tables.eq(row),
652            None => eq_index(self.r, row),
653        }
654    }
655
656    /// MLE of one cadence block: an interval of the eq
657    /// tables at stride 1, cached residue sums otherwise.
658    fn cadence(&mut self, stride: usize, count: usize, origin: usize, values: &[F]) -> Flat<F> {
659        let zero = Flat::from_raw(F::ZERO);
660
661        if stride == 0 || values.len() != stride {
662            return zero;
663        }
664
665        let end = origin.saturating_add(stride.saturating_mul(count));
666
667        let Self { r, eq, sums } = self;
668
669        match (stride, eq) {
670            (1, Some(tables)) => scaled(values[0], tables.below(end) + tables.below(origin)),
671            _ => {
672                for limit in [end, origin] {
673                    sums.entry((stride, limit))
674                        .or_insert_with(|| residue_sums(r, limit, stride, None));
675                }
676
677                let upper = &sums[&(stride, end)];
678                let lower = &sums[&(stride, origin)];
679                let shift = stride - origin % stride;
680
681                let mut acc = zero;
682                for (c, (&u, &l)) in upper.iter().zip(lower).enumerate() {
683                    acc += scaled(values[(c + shift) % stride], u + l);
684                }
685
686                acc
687            }
688        }
689    }
690}
691
692/// `eq(r, ·)` over the low and the high
693/// half of `r`, each with its prefix sums.
694struct EqTables<F> {
695    low_bits: usize,
696    eq_low: Vec<Flat<F>>,
697    eq_high: Vec<Flat<F>>,
698    below_low: Vec<Flat<F>>,
699    below_high: Vec<Flat<F>>,
700}
701
702impl<F: HardwareField> EqTables<F> {
703    fn new(r: &[Flat<F>]) -> Self {
704        let low_bits = r.len() / 2;
705        let eq_low = eq_table(&r[..low_bits]);
706        let eq_high = eq_table(&r[low_bits..]);
707
708        Self {
709            low_bits,
710            below_low: running_sums(&eq_low),
711            below_high: running_sums(&eq_high),
712            eq_low,
713            eq_high,
714        }
715    }
716
717    /// `eq(r, row)`, ignoring the bits of `row` past `r.len()`.
718    fn eq(&self, row: usize) -> Flat<F> {
719        let low = row & (self.eq_low.len() - 1);
720        let high = (row >> self.low_bits) & (self.eq_high.len() - 1);
721
722        self.eq_low[low] * self.eq_high[high]
723    }
724
725    /// `Σ_{x < limit} eq(r, x)`; one once `limit` reaches `2^r.len()`.
726    fn below(&self, limit: usize) -> Flat<F> {
727        let high = limit >> self.low_bits;
728
729        if high >= self.eq_high.len() {
730            return Flat::from_raw(F::ONE);
731        }
732
733        let low = limit & (self.eq_low.len() - 1);
734
735        self.below_high[high] + self.eq_high[high] * self.below_low[low]
736    }
737}
738
739/// Declares a fixed column from a shape.
740pub fn fix<F>(col_idx: usize, shape: FixedShape<F>) -> FixedColumn<F> {
741    FixedColumn { col_idx, shape }
742}
743
744/// Rejects out-of-range `col_idx`, duplicate pins,
745/// malformed shapes, and out-of-domain values
746/// (`Bit` columns require values in {0, 1}).
747pub fn validate_fixed_columns<F: TowerField>(
748    fixed: &[FixedColumn<F>],
749    layout: &[ColumnType],
750    num_vars: Option<usize>,
751) -> errors::Result<()> {
752    for (i, fc) in fixed.iter().enumerate() {
753        if fc.col_idx >= layout.len() {
754            return Err(errors::Error::Protocol {
755                protocol: "fixed_column",
756                message: "col_idx out of range",
757            });
758        }
759
760        validate_shape(&fc.shape, layout[fc.col_idx], num_vars)?;
761
762        for prior in &fixed[..i] {
763            if prior.col_idx == fc.col_idx {
764                return Err(errors::Error::Protocol {
765                    protocol: "fixed_column",
766                    message: "duplicate pin on same column",
767                });
768            }
769        }
770    }
771
772    Ok(())
773}
774
775/// `Σ_{x < limit, x ≡ c (mod stride)} eq(r, x) · W^⌊x / stride⌋`
776/// per residue `c`, from `steps[t] = W^(2^t)`; `W = 1` without steps.
777fn residue_sums<F: HardwareField>(
778    r: &[Flat<F>],
779    limit: usize,
780    stride: usize,
781    steps: Option<&[Flat<F>]>,
782) -> Vec<Flat<F>> {
783    let one = Flat::from_raw(F::ONE);
784    let zero = Flat::from_raw(F::ZERO);
785
786    let full = match 1usize.checked_shl(r.len() as u32) {
787        Some(cap) => limit >= cap,
788        None => false,
789    };
790
791    let mut free = vec![zero; stride];
792    let mut next = vec![zero; stride];
793
794    let mut tight = if full { zero } else { one };
795    let mut tight_residue = 0usize;
796
797    if full {
798        free[0] = one;
799    }
800
801    for t in (0..r.len()).rev() {
802        let r_t = r[t];
803        let step = steps.map(|steps| steps[t]);
804
805        next.fill(zero);
806
807        for (residue, &weight) in free.iter().enumerate() {
808            let low = 2 * residue;
809            let hi = weight * r_t;
810
811            let (at, w) = division_step(low, weight + hi, stride, step);
812            next[at] += w;
813
814            let (at, w) = division_step(low + 1, hi, stride, step);
815            next[at] += w;
816        }
817
818        let low = 2 * tight_residue;
819        let hi = tight * r_t;
820
821        (tight_residue, tight) = match (limit >> t) & 1 {
822            1 => {
823                let (at, w) = division_step(low, tight + hi, stride, step);
824                next[at] += w;
825
826                division_step(low + 1, hi, stride, step)
827            }
828            _ => division_step(low, tight + hi, stride, step),
829        };
830
831        core::mem::swap(&mut free, &mut next);
832    }
833
834    free
835}
836
837fn division_step<F: HardwareField>(
838    value: usize,
839    weight: Flat<F>,
840    stride: usize,
841    step: Option<Flat<F>>,
842) -> (usize, Flat<F>) {
843    match (value >= stride, step) {
844        (true, Some(step)) => (value - stride, weight * step),
845        (true, None) => (value - stride, weight),
846        (false, _) => (value, weight),
847    }
848}
849
850fn cadence_mle<F: HardwareField>(
851    r: &[Flat<F>],
852    stride: usize,
853    count: usize,
854    origin: usize,
855    values: &[F],
856) -> Flat<F> {
857    if stride == 0 || values.len() != stride {
858        return Flat::from_raw(F::ZERO);
859    }
860
861    let end = origin.saturating_add(stride.saturating_mul(count));
862
863    let mut class_sums = residue_sums(r, end, stride, None);
864    let start_sums = residue_sums(r, origin, stride, None);
865
866    for (c, s) in start_sums.iter().enumerate() {
867        class_sums[c] += *s;
868    }
869
870    let mut acc = Flat::from_raw(F::ZERO);
871    for (c, sum) in class_sums.iter().enumerate() {
872        let offset = (c + stride - origin % stride) % stride;
873        acc += values[offset].to_hardware() * *sum;
874    }
875
876    acc
877}
878
879fn cadence_value_at<F: HardwareField>(
880    row: usize,
881    stride: usize,
882    count: usize,
883    origin: usize,
884    values: &[F],
885) -> Flat<F> {
886    if stride == 0 || values.len() != stride {
887        return Flat::from_raw(F::ZERO);
888    }
889
890    let end = origin.saturating_add(stride.saturating_mul(count));
891
892    if row < origin || row >= end {
893        Flat::from_raw(F::ZERO)
894    } else {
895        values[(row - origin) % stride].to_hardware()
896    }
897}
898
899fn eq_index<F: HardwareField>(r: &[Flat<F>], index: usize) -> Flat<F> {
900    let one = Flat::from_raw(F::ONE);
901    let mut prod = one;
902
903    for (k, &r_k) in r.iter().enumerate() {
904        let factor = if (index >> k) & 1 == 1 {
905            r_k
906        } else {
907            one - r_k
908        };
909
910        prod *= factor;
911    }
912
913    prod
914}
915
916/// `eq(r, x)` for every `x < 2^r.len()`,
917/// bit `k` of `x` paired with `r[k]`.
918fn eq_table<F: HardwareField>(r: &[Flat<F>]) -> Vec<Flat<F>> {
919    let mut table = Vec::with_capacity(1 << r.len());
920    table.push(Flat::from_raw(F::ONE));
921
922    for &r_k in r {
923        for i in 0..table.len() {
924            let hi = table[i] * r_k;
925
926            table[i] += hi;
927            table.push(hi);
928        }
929    }
930
931    table
932}
933
934/// `sums[k] = Σ_{i < k} values[i]` for `k = 0..=len`.
935fn running_sums<F: HardwareField>(values: &[Flat<F>]) -> Vec<Flat<F>> {
936    let mut sums = Vec::with_capacity(values.len() + 1);
937    let mut acc = Flat::from_raw(F::ZERO);
938
939    sums.push(acc);
940
941    for &v in values {
942        acc += v;
943
944        sums.push(acc);
945    }
946
947    sums
948}
949
950/// `value · x`, skipping the basis conversion when `value` is zero or one.
951fn scaled<F: HardwareField>(value: F, x: Flat<F>) -> Flat<F> {
952    if value == F::ZERO {
953        Flat::from_raw(F::ZERO)
954    } else if value == F::ONE {
955        x
956    } else {
957        value.to_hardware() * x
958    }
959}
960
961fn validate_shape<F: TowerField>(
962    shape: &FixedShape<F>,
963    col_type: ColumnType,
964    num_vars: Option<usize>,
965) -> errors::Result<()> {
966    match shape {
967        FixedShape::LastRow | FixedShape::FirstRow => Ok(()),
968        FixedShape::Custom(bits) => match num_vars {
969            Some(nv) if bits.len() != nv => Err(errors::Error::Protocol {
970                protocol: "fixed_column",
971                message: "Custom point bit width != num_vars",
972            }),
973            _ => Ok(()),
974        },
975        FixedShape::Periodic { period, values } => {
976            if !period.is_power_of_two() {
977                return Err(errors::Error::Protocol {
978                    protocol: "fixed_column",
979                    message: "Periodic period must be a power of two",
980                });
981            }
982
983            if values.len() != *period {
984                return Err(errors::Error::Protocol {
985                    protocol: "fixed_column",
986                    message: "Periodic values length != period",
987                });
988            }
989
990            if let Some(nv) = num_vars
991                && *period > (1usize << nv)
992            {
993                return Err(errors::Error::Protocol {
994                    protocol: "fixed_column",
995                    message: "Periodic period exceeds trace height",
996                });
997            }
998
999            check_bit_domain(values.iter().copied(), col_type)
1000        }
1001        FixedShape::Sparse(entries) => {
1002            if let Some(nv) = num_vars {
1003                let n = 1usize << nv;
1004                for &(row, _) in entries {
1005                    if row >= n {
1006                        return Err(errors::Error::Protocol {
1007                            protocol: "fixed_column",
1008                            message: "Sparse row index exceeds trace height",
1009                        });
1010                    }
1011                }
1012            }
1013
1014            let mut rows: Vec<usize> = entries.iter().map(|&(row, _)| row).collect();
1015            rows.sort_unstable();
1016
1017            if rows.windows(2).any(|pair| pair[0] == pair[1]) {
1018                return Err(errors::Error::Protocol {
1019                    protocol: "fixed_column",
1020                    message: "duplicate Sparse row",
1021                });
1022            }
1023
1024            check_bit_domain(entries.iter().map(|&(_, v)| v), col_type)
1025        }
1026        FixedShape::Dense(values) => {
1027            if let Some(nv) = num_vars
1028                && values.len() != (1usize << nv)
1029            {
1030                return Err(errors::Error::Protocol {
1031                    protocol: "fixed_column",
1032                    message: "Dense values length != trace height",
1033                });
1034            }
1035
1036            check_bit_domain(values.iter().copied(), col_type)
1037        }
1038        FixedShape::Cadence {
1039            stride,
1040            count,
1041            origin,
1042            values,
1043        } => {
1044            if *stride == 0 {
1045                return Err(errors::Error::Protocol {
1046                    protocol: "fixed_column",
1047                    message: "Cadence stride must be non-zero",
1048                });
1049            }
1050
1051            if values.len() != *stride {
1052                return Err(errors::Error::Protocol {
1053                    protocol: "fixed_column",
1054                    message: "Cadence values length != stride",
1055                });
1056            }
1057
1058            let end = stride
1059                .checked_mul(*count)
1060                .and_then(|span| span.checked_add(*origin))
1061                .ok_or(errors::Error::Protocol {
1062                    protocol: "fixed_column",
1063                    message: "Cadence active range overflows",
1064                })?;
1065
1066            if let Some(nv) = num_vars
1067                && end > (1usize << nv)
1068            {
1069                return Err(errors::Error::Protocol {
1070                    protocol: "fixed_column",
1071                    message: "Cadence active range exceeds trace height",
1072                });
1073            }
1074
1075            check_bit_domain(values.iter().copied(), col_type)
1076        }
1077        FixedShape::Segments(segments) => {
1078            if segments.is_empty() {
1079                return Err(errors::Error::Protocol {
1080                    protocol: "fixed_column",
1081                    message: "Segments list is empty",
1082                });
1083            }
1084
1085            let mut prev_end = 0usize;
1086            for seg in segments {
1087                if seg.stride == 0 {
1088                    return Err(errors::Error::Protocol {
1089                        protocol: "fixed_column",
1090                        message: "Segments stride must be non-zero",
1091                    });
1092                }
1093
1094                if seg.count == 0 {
1095                    return Err(errors::Error::Protocol {
1096                        protocol: "fixed_column",
1097                        message: "Segments count must be non-zero",
1098                    });
1099                }
1100
1101                if seg.values.len() != seg.stride {
1102                    return Err(errors::Error::Protocol {
1103                        protocol: "fixed_column",
1104                        message: "Segments values length != stride",
1105                    });
1106                }
1107
1108                let end = seg
1109                    .stride
1110                    .checked_mul(seg.count)
1111                    .and_then(|span| span.checked_add(seg.origin))
1112                    .ok_or(errors::Error::Protocol {
1113                        protocol: "fixed_column",
1114                        message: "Segments active range overflows",
1115                    })?;
1116
1117                if seg.origin < prev_end {
1118                    return Err(errors::Error::Protocol {
1119                        protocol: "fixed_column",
1120                        message: "Segments must be sorted by origin and disjoint",
1121                    });
1122                }
1123
1124                prev_end = end;
1125
1126                check_bit_domain(seg.values.iter().copied(), col_type)?;
1127            }
1128
1129            if let Some(nv) = num_vars
1130                && prev_end > (1usize << nv)
1131            {
1132                return Err(errors::Error::Protocol {
1133                    protocol: "fixed_column",
1134                    message: "Segments active range exceeds trace height",
1135                });
1136            }
1137
1138            Ok(())
1139        }
1140    }
1141}
1142
1143fn check_bit_domain<F: TowerField>(
1144    values: impl Iterator<Item = F>,
1145    col_type: ColumnType,
1146) -> errors::Result<()> {
1147    if col_type != ColumnType::Bit {
1148        return Ok(());
1149    }
1150
1151    for v in values {
1152        if v != F::ZERO && v != F::ONE {
1153            return Err(errors::Error::Protocol {
1154                protocol: "fixed_column",
1155                message: "Bit fixed column value not in {0,1}",
1156            });
1157        }
1158    }
1159
1160    Ok(())
1161}