Skip to main content

miden_air/constraints/chiplets/
columns.rs

1//! Column structs for all chiplet sub-components and periodic columns.
2
3use alloc::{vec, vec::Vec};
4use core::{
5    borrow::{Borrow, BorrowMut},
6    mem::size_of,
7};
8
9use miden_core::{Felt, WORD_SIZE, chiplets::hasher::Hasher, field::PrimeCharacteristicRing};
10
11use super::super::{columns::indices_arr, ext_field::QuadFeltExpr};
12use crate::trace::chiplets::{
13    bitwise::NUM_DECOMP_BITS,
14    hasher::{CAPACITY_LEN, DIGEST_LEN, HASH_CYCLE_LEN, NUM_SELECTORS, RATE_LEN, STATE_WIDTH},
15};
16
17// HELPERS
18// ================================================================================================
19
20/// Generates `Borrow<$cols<T>> for [T]` and the mutable counterpart for a chiplet column
21/// struct. The slice length must equal `size_of::<$cols<u8>>()` cells.
22macro_rules! impl_borrow_for_chiplet_cols {
23    ($cols:ident) => {
24        impl<T> Borrow<$cols<T>> for [T] {
25            fn borrow(&self) -> &$cols<T> {
26                debug_assert_eq!(self.len(), size_of::<$cols<u8>>());
27                let (prefix, cols, suffix) = unsafe { self.align_to::<$cols<T>>() };
28                debug_assert!(prefix.is_empty() && suffix.is_empty() && cols.len() == 1);
29                &cols[0]
30            }
31        }
32        impl<T> BorrowMut<$cols<T>> for [T] {
33            fn borrow_mut(&mut self) -> &mut $cols<T> {
34                debug_assert_eq!(self.len(), size_of::<$cols<u8>>());
35                let (prefix, cols, suffix) = unsafe { self.align_to_mut::<$cols<T>>() };
36                debug_assert!(prefix.is_empty() && suffix.is_empty() && cols.len() == 1);
37                &mut cols[0]
38            }
39        }
40    };
41}
42
43// PERMUTATION COLUMNS
44// ================================================================================================
45
46/// Permutation chiplet columns (19 columns), viewed from `chiplets[0..19]`.
47///
48/// Logical overlay for permutation segment rows (`s_00 = 1`). The 3 witness columns
49/// `w0..w2` share the same physical columns as the controller's `s0/s1/s2` selectors,
50/// and `multiplicity` shares the same physical column as the controller's `node_index`.
51///
52/// `s_00` and `s_01` (= `ChipletCols::s_00` and `ChipletCols::s_01`) are consumed by the chiplet
53/// selector system and are NOT part of this overlay.
54///
55/// The state holds a Poseidon2 sponge in `[RATE0, RATE1, CAPACITY]` layout.
56/// Helper methods `rate0()`, `rate1()`, `capacity()`, and `digest()` provide
57/// sub-views into the state array.
58///
59/// ## Layout
60///
61/// ```text
62/// | witnesses[3] | state[12]                                    | extra cols      |
63/// |              | rate0[4] (= digest) | rate1[4] | capacity[4] |                 |
64/// | w0, w1, w2   | h0..h3              | h4..h7   | h8..h11     | m  --  --  --   |
65/// ```
66#[repr(C)]
67#[derive(Clone, Debug)]
68pub struct PermutationCols<T> {
69    /// S-box witness columns (same physical columns as hasher selectors).
70    pub witnesses: [T; NUM_SELECTORS],
71    /// Poseidon2 state (12 field elements: 8 rate + 4 capacity).
72    pub state: [T; STATE_WIDTH],
73    /// Request multiplicity (same physical column as node_index).
74    pub multiplicity: T,
75    /// Physical slots for controller columns mrupdate_id, is_boundary, and direction_bit.
76    /// These must be zero on permutation rows; access via [`Self::unused_padding()`] only.
77    _unused: [T; 3],
78}
79
80impl<T: Copy> PermutationCols<T> {
81    /// Returns the rate portion of the state (state[0..8]).
82    pub fn rate(&self) -> [T; RATE_LEN] {
83        [
84            self.state[0],
85            self.state[1],
86            self.state[2],
87            self.state[3],
88            self.state[4],
89            self.state[5],
90            self.state[6],
91            self.state[7],
92        ]
93    }
94
95    /// Returns the capacity portion of the state (state[8..12]).
96    pub fn capacity(&self) -> [T; CAPACITY_LEN] {
97        [self.state[8], self.state[9], self.state[10], self.state[11]]
98    }
99
100    /// Returns the digest portion of the state (state[0..4]).
101    pub fn digest(&self) -> [T; DIGEST_LEN] {
102        [self.state[0], self.state[1], self.state[2], self.state[3]]
103    }
104
105    /// Returns rate0 (state[0..4]).
106    pub fn rate0(&self) -> [T; DIGEST_LEN] {
107        [self.state[0], self.state[1], self.state[2], self.state[3]]
108    }
109
110    /// Returns rate1 (state[4..8]).
111    pub fn rate1(&self) -> [T; DIGEST_LEN] {
112        [self.state[4], self.state[5], self.state[6], self.state[7]]
113    }
114
115    /// Returns the 3 padding columns (mrupdate_id, is_boundary, direction_bit) that must
116    /// be zero on permutation rows.
117    pub fn unused_padding(&self) -> [T; 3] {
118        self._unused
119    }
120
121    /// Sets the 3 padding columns (mrupdate_id, is_boundary, direction_bit) to the given value.
122    pub fn set_unused_padding(&mut self, value: T) {
123        self._unused.fill(value);
124    }
125}
126
127// CONTROLLER COLUMNS
128// ================================================================================================
129
130/// Controller chiplet columns (19 columns), viewed from `chiplets[0..19]`.
131///
132/// Logical overlay for controller rows (`s_01 = 1`). `s0` distinguishes input rows
133/// (`s0 = 1`) from output/padding rows (`s0 = 0`). The physical layout mirrors
134/// [`PermutationCols`], but column names reflect the controller/permutation split.
135///
136/// `s_00` and `s_01` (= `ChipletCols::s_00` and `ChipletCols::s_01`) are consumed by the chiplet
137/// selector system and are NOT part of this overlay. Because the chiplet-level
138/// non-hasher selector is only ever a virtual expression (`1 - s_00 - s_01`) and is
139/// never a named column or struct field, there is no name collision with the
140/// controller-internal `s0` defined here.
141///
142/// The state holds a Poseidon2 sponge in `[RATE0, RATE1, CAPACITY]` layout.
143/// Helper methods `rate0()`, `rate1()`, `capacity()`, and `digest()` provide
144/// sub-views into the state array.
145///
146/// ## Layout
147///
148/// ```text
149/// | s0 s1 s2 | state[12]                                    | extra cols      |
150/// |          | rate0[4] (= digest) | rate1[4] | capacity[4] |                 |
151/// |          | h0..h3              | h4..h7   | h8..h11     | i  mr  bnd  dir |
152/// ```
153#[repr(C)]
154#[derive(Clone, Debug)]
155pub struct ControllerCols<T> {
156    /// Hasher-internal sub-selector: `s0 = 1` on controller input rows, 0 on output/padding.
157    pub s0: T,
158    /// Operation sub-selector s1.
159    pub s1: T,
160    /// Operation sub-selector s2.
161    pub s2: T,
162    /// Poseidon2 state (12 field elements: 8 rate + 4 capacity).
163    pub state: [T; STATE_WIDTH],
164    /// Merkle tree node index.
165    pub node_index: T,
166    /// Domain separator for sibling table across MRUPDATE ops.
167    pub mrupdate_id: T,
168    /// 1 on boundary rows (first input or last output of each permutation).
169    pub is_boundary: T,
170    /// Direction bit for Merkle path verification.
171    pub direction_bit: T,
172}
173
174impl<T: Copy> ControllerCols<T> {
175    /// Returns the rate portion of the state (state[0..8]).
176    pub fn rate(&self) -> [T; RATE_LEN] {
177        [
178            self.state[0],
179            self.state[1],
180            self.state[2],
181            self.state[3],
182            self.state[4],
183            self.state[5],
184            self.state[6],
185            self.state[7],
186        ]
187    }
188
189    /// Returns the capacity portion of the state (state[8..12]).
190    pub fn capacity(&self) -> [T; CAPACITY_LEN] {
191        [self.state[8], self.state[9], self.state[10], self.state[11]]
192    }
193
194    /// Returns the digest portion of the state (state[0..4]).
195    pub fn digest(&self) -> [T; DIGEST_LEN] {
196        [self.state[0], self.state[1], self.state[2], self.state[3]]
197    }
198
199    /// Returns rate0 (state[0..4]).
200    pub fn rate0(&self) -> [T; DIGEST_LEN] {
201        [self.state[0], self.state[1], self.state[2], self.state[3]]
202    }
203
204    /// Returns rate1 (state[4..8]).
205    pub fn rate1(&self) -> [T; DIGEST_LEN] {
206        [self.state[4], self.state[5], self.state[6], self.state[7]]
207    }
208
209    /// Merkle-update new-path flag: `s0 * s1 * s2`.
210    ///
211    /// Active on controller input rows that insert the new Merkle path into the sibling
212    /// table (request/remove side of the sibling bus).
213    pub fn f_mu<E: PrimeCharacteristicRing>(&self) -> E
214    where
215        T: Into<E>,
216    {
217        self.s0.into() * self.s1.into() * self.s2.into()
218    }
219
220    /// Merkle-verify / old-path flag: `s0 * s1 * (1 - s2)`.
221    ///
222    /// Active on controller input rows that extract the old Merkle path from the sibling
223    /// table (response/add side of the sibling bus).
224    pub fn f_mv<E: PrimeCharacteristicRing>(&self) -> E
225    where
226        T: Into<E>,
227    {
228        self.s0.into() * self.s1.into() * (E::ONE - self.s2.into())
229    }
230}
231
232// BITWISE COLUMNS
233// ================================================================================================
234
235/// Bitwise chiplet columns (13 columns), viewed from `chiplets[1..14]`.
236///
237/// Bit decomposition columns (`a_bits`, `b_bits`) are in **little-endian** order:
238/// `value = bits[0] + 2*bits[1] + 4*bits[2] + 8*bits[3]`.
239#[repr(C)]
240#[derive(Clone, Debug)]
241pub struct BitwiseCols<T> {
242    /// Operation flag: 0 = AND, 1 = XOR.
243    pub op_flag: T,
244    /// Aggregated input a.
245    pub a: T,
246    /// Aggregated input b.
247    pub b: T,
248    /// 4-bit decomposition of a.
249    pub a_bits: [T; NUM_DECOMP_BITS],
250    /// 4-bit decomposition of b.
251    pub b_bits: [T; NUM_DECOMP_BITS],
252    /// Previous aggregated output.
253    pub prev_output: T,
254    /// Current aggregated output.
255    pub output: T,
256}
257
258// MEMORY COLUMNS
259// ================================================================================================
260
261/// Memory chiplet columns (15 columns), viewed from `chiplets[2..17]`.
262///
263/// When reading from a new word address (first access to a context/addr pair), the
264/// `values` are initialized to zero.
265#[repr(C)]
266#[derive(Clone, Debug)]
267pub struct MemoryCols<T> {
268    /// Read/write flag (0 = write, 1 = read).
269    pub is_read: T,
270    /// Element/word flag (0 = element, 1 = word).
271    pub is_word: T,
272    /// Memory context ID.
273    pub ctx: T,
274    /// Word address.
275    pub word_addr: T,
276    /// First bit of the address index within the word.
277    pub idx0: T,
278    /// Second bit of the address index within the word.
279    pub idx1: T,
280    /// Clock cycle of the memory access.
281    pub clk: T,
282    /// Values stored at this context/word/clock after the operation.
283    pub values: [T; WORD_SIZE],
284    /// Lower 16 bits of delta.
285    pub d0: T,
286    /// Upper 16 bits of delta.
287    pub d1: T,
288    /// Inverse of delta.
289    pub d_inv: T,
290    /// Flag: same context and same word address as previous operation (docs: `f_sca`).
291    pub is_same_ctx_and_addr: T,
292}
293
294// ACE COLUMNS
295// ================================================================================================
296
297/// ACE chiplet columns (16 columns), viewed from `chiplets[3..19]`.
298///
299/// The ACE (Arithmetic Circuit Evaluator) chiplet evaluates arithmetic circuits over
300/// quadratic extension field elements. Each circuit evaluation consists of two phases:
301///
302/// 1. **READ** (`s_block=0`): loads wire values from memory into the chiplet.
303/// 2. **EVAL** (`s_block=1`): evaluates arithmetic gates on loaded wire values.
304///
305/// The first 12 columns are common to both modes. The last 4 (`mode`) are overlaid
306/// and reinterpreted depending on `s_block`:
307///
308/// ```text
309/// mode idx | READ (s_block=0)       | EVAL (s_block=1)
310/// ---------+------------------------+-------------------
311///  0       | num_eval               | id_2
312///  1       | (unused)               | v_2.0
313///  2       | m_1 (wire-1 mult)      | v_2.1
314///  3       | m_0 (wire-0 mult)      | m_0 (wire-0 mult)
315/// ```
316///
317/// Use `ace.read()` / `ace.eval()` for typed overlays of the mode columns.
318#[repr(C)]
319#[derive(Clone, Debug)]
320pub struct AceCols<T> {
321    /// Start-of-circuit flag (1 on the first row of a new circuit evaluation).
322    pub s_start: T,
323    /// Block selector: 0 = READ (memory loads), 1 = EVAL (gate evaluation).
324    pub s_block: T,
325    /// Memory context for the current circuit evaluation.
326    pub ctx: T,
327    /// Memory pointer from which to read the next two wire values or instruction.
328    pub ptr: T,
329    /// Clock cycle at which the memory read is performed.
330    pub clk: T,
331    /// Arithmetic operation selector (determines which gate to evaluate in EVAL mode).
332    pub eval_op: T,
333    /// ID of the first wire (output wire / left operand).
334    pub id_0: T,
335    /// Value of the first wire (quadratic extension field element).
336    pub v_0: QuadFeltExpr<T>,
337    /// ID of the second wire (first input / left operand).
338    pub id_1: T,
339    /// Value of the second wire (quadratic extension field element).
340    pub v_1: QuadFeltExpr<T>,
341    /// Mode-dependent columns (interpretation depends on `s_block`; see table above).
342    mode: [T; 4],
343}
344
345impl<T> AceCols<T> {
346    /// Returns a READ-mode overlay of the mode-dependent columns.
347    pub fn read(&self) -> &AceReadCols<T> {
348        self.mode.as_slice().borrow()
349    }
350
351    /// Returns an EVAL-mode overlay of the mode-dependent columns.
352    pub fn eval(&self) -> &AceEvalCols<T> {
353        self.mode.as_slice().borrow()
354    }
355
356    /// Returns a mutable READ-mode overlay of the mode-dependent columns.
357    pub fn read_mut(&mut self) -> &mut AceReadCols<T> {
358        self.mode.as_mut_slice().borrow_mut()
359    }
360
361    /// Returns a mutable EVAL-mode overlay of the mode-dependent columns.
362    pub fn eval_mut(&mut self) -> &mut AceEvalCols<T> {
363        self.mode.as_mut_slice().borrow_mut()
364    }
365}
366
367impl<T: Copy> AceCols<T> {
368    /// ACE read flag: `1 - s_block`.
369    ///
370    /// Active on ACE rows in READ mode (memory word reads for circuit inputs).
371    pub fn f_read<E: PrimeCharacteristicRing>(&self) -> E
372    where
373        T: Into<E>,
374    {
375        E::ONE - self.s_block.into()
376    }
377
378    /// ACE eval flag: `s_block`.
379    ///
380    /// Active on ACE rows in EVAL mode (circuit gate evaluation).
381    pub fn f_eval<E: PrimeCharacteristicRing>(&self) -> E
382    where
383        T: Into<E>,
384    {
385        self.s_block.into()
386    }
387}
388
389/// READ mode overlay for ACE mode-dependent columns (4 columns).
390///
391/// In READ mode, the chiplet loads wire values from memory. The multiplicity columns
392/// (`m_0`, `m_1`) track how many times each wire participates in circuit gates, used
393/// by the wiring bus to verify correct wire connections.
394#[repr(C)]
395#[derive(Clone, Debug)]
396pub struct AceReadCols<T> {
397    /// Number of EVAL rows that follow this READ block.
398    pub num_eval: T,
399    /// Unused column (padding for layout alignment with EVAL overlay).
400    pub unused: T,
401    /// Multiplicity of the second wire (wire 1).
402    pub m_1: T,
403    /// Multiplicity of the first wire (wire 0).
404    pub m_0: T,
405}
406
407/// EVAL mode overlay for ACE mode-dependent columns (4 columns).
408///
409/// In EVAL mode, the chiplet evaluates an arithmetic gate on three wires: two inputs
410/// (`id_1`, `id_2`) and one output (`id_0`). The third wire's ID and value occupy the
411/// same physical columns as `num_eval`/`unused`/`m_1` in READ mode.
412#[repr(C)]
413#[derive(Clone, Debug)]
414pub struct AceEvalCols<T> {
415    /// ID of the third wire (second input / right operand).
416    pub id_2: T,
417    /// Value of the third wire.
418    pub v_2: QuadFeltExpr<T>,
419    /// Multiplicity of the first wire (wire 0).
420    pub m_0: T,
421}
422
423// ACE COLUMN INDEX MAPS
424// ================================================================================================
425
426/// Compile-time index map for the top-level ACE chiplet columns (16 columns).
427#[allow(dead_code)]
428pub const ACE_COL_MAP: AceCols<usize> = {
429    assert!(size_of::<AceCols<u8>>() == 16);
430    unsafe { core::mem::transmute(indices_arr::<{ size_of::<AceCols<u8>>() }>()) }
431};
432
433/// Compile-time index map for the READ overlay (relative to `mode`).
434pub const ACE_READ_COL_MAP: AceReadCols<usize> = {
435    assert!(size_of::<AceReadCols<u8>>() == 4);
436    unsafe { core::mem::transmute(indices_arr::<{ size_of::<AceReadCols<u8>>() }>()) }
437};
438
439/// Compile-time index map for the EVAL overlay (relative to `mode`).
440pub const ACE_EVAL_COL_MAP: AceEvalCols<usize> = {
441    assert!(size_of::<AceEvalCols<u8>>() == 4);
442    unsafe { core::mem::transmute(indices_arr::<{ size_of::<AceEvalCols<u8>>() }>()) }
443};
444
445/// Offset of the `mode` array within the ACE chiplet columns.
446#[allow(dead_code)]
447pub const MODE_OFFSET: usize = ACE_COL_MAP.mode[0];
448
449const _: () = {
450    assert!(size_of::<AceCols<u8>>() == 16);
451    assert!(size_of::<AceReadCols<u8>>() == 4);
452    assert!(size_of::<AceEvalCols<u8>>() == 4);
453
454    // m_0 is at the same position in both overlays.
455    assert!(ACE_READ_COL_MAP.m_0 == ACE_EVAL_COL_MAP.m_0);
456
457    // READ-only and EVAL-only columns overlap at the expected positions.
458    assert!(ACE_READ_COL_MAP.num_eval == ACE_EVAL_COL_MAP.id_2);
459    assert!(ACE_READ_COL_MAP.m_1 == ACE_EVAL_COL_MAP.v_2.1);
460};
461
462// KERNEL ROM COLUMNS
463// ================================================================================================
464
465/// Kernel ROM chiplet columns (5 columns), viewed from `chiplets[4..9]`.
466#[repr(C)]
467#[derive(Clone, Debug)]
468pub struct KernelRomCols<T> {
469    /// Number of SYSCALLs to this procedure (CALL-label multiplicity).
470    pub multiplicity: T,
471    /// Kernel procedure root digest.
472    pub root: [T; WORD_SIZE],
473}
474
475// PERIODIC COLUMNS
476// ================================================================================================
477
478/// All chiplet periodic columns (20 columns).
479///
480/// Aggregates hasher (18 columns) and bitwise (2 columns) periodic values into a single
481/// typed view. Use `builder.periodic_values().borrow()` to obtain a `&PeriodicCols<_>`.
482#[derive(Clone, Copy)]
483#[repr(C)]
484pub struct PeriodicCols<T> {
485    /// Hasher periodic columns (cycle markers, step selectors, round constants).
486    pub hasher: HasherPeriodicCols<T>,
487    /// Bitwise periodic columns.
488    pub bitwise: BitwisePeriodicCols<T>,
489}
490
491/// Hasher chiplet periodic columns (16 columns, period = 16 rows).
492///
493/// Provides step-type selectors and Poseidon2 round constants for the hasher chiplet.
494/// The hasher operates on a 16-row cycle (15 transitions + 1 boundary row).
495///
496/// ## Layout
497///
498/// | Index | Name           | Description |
499/// |-------|----------------|-------------|
500/// | 0     | is_init_ext    | 1 on row 0 (init linear + first external round) |
501/// | 1     | is_ext         | 1 on rows 1-3, 12-14 (single external round) |
502/// | 2     | is_packed_int  | 1 on rows 4-10 (3 packed internal rounds) |
503/// | 3     | is_int_ext     | 1 on row 11 (int22 + ext5 merged) |
504/// | 4-15  | ark[0..12]     | Shared round constants |
505#[derive(Clone, Copy)]
506#[repr(C)]
507pub struct HasherPeriodicCols<T> {
508    /// 1 on row 0 (init linear + first external round).
509    pub is_init_ext: T,
510    /// 1 on rows 1-3, 12-14 (single external round).
511    pub is_ext: T,
512    /// 1 on rows 4-10 (3 packed internal rounds).
513    pub is_packed_int: T,
514    /// 1 on row 11 (int22 + ext5 merged).
515    pub is_int_ext: T,
516    /// Shared round constants (12 lanes). Carry external round constants on external
517    /// rows, and internal round constants in ark[0..2] on packed-internal rows.
518    pub ark: [T; STATE_WIDTH],
519}
520
521/// Bitwise chiplet periodic columns (2 columns, period = 8 rows).
522#[derive(Clone, Copy)]
523#[repr(C)]
524pub struct BitwisePeriodicCols<T> {
525    /// Marks first row of 8-row cycle: `[1, 0, 0, 0, 0, 0, 0, 0]`.
526    pub k_first: T,
527    /// Marks non-last rows of 8-row cycle: `[1, 1, 1, 1, 1, 1, 1, 0]`.
528    pub k_transition: T,
529}
530
531// PERIODIC COLUMN GENERATION
532// ================================================================================================
533
534impl Default for HasherPeriodicCols<Vec<Felt>> {
535    fn default() -> Self {
536        Self::new()
537    }
538}
539
540impl HasherPeriodicCols<Vec<Felt>> {
541    /// Generate periodic columns for the Poseidon2 hasher chiplet.
542    ///
543    /// All columns repeat every 16 rows, matching one permutation cycle.
544    ///
545    /// The 4 selector columns identify the row type. The 12 ark columns carry either
546    /// external round constants (on external rows) or internal round constants in
547    /// `ark[0..2]` (on packed-internal rows).
548    ///
549    /// ## 16-Row Schedule
550    ///
551    /// ```text
552    /// Row  Transition              Selector
553    /// 0    init + ext1             is_init_ext
554    /// 1-3  ext2-ext4               is_ext
555    /// 4-10 3x packed internal      is_packed_int
556    /// 11   int22 + ext5            is_int_ext
557    /// 12-14 ext6-ext8              is_ext
558    /// 15   boundary                (none)
559    /// ```
560    #[expect(
561        clippy::needless_range_loop,
562        reason = "index-based assignments mirror the documented 16-row schedule"
563    )]
564    pub fn new() -> Self {
565        // -------------------------------------------------------------------------
566        // Selectors
567        // -------------------------------------------------------------------------
568        let mut is_init_ext = vec![Felt::ZERO; HASH_CYCLE_LEN];
569        let mut is_ext = vec![Felt::ZERO; HASH_CYCLE_LEN];
570        let mut is_packed_int = vec![Felt::ZERO; HASH_CYCLE_LEN];
571        let mut is_int_ext = vec![Felt::ZERO; HASH_CYCLE_LEN];
572
573        is_init_ext[0] = Felt::ONE;
574
575        for r in [1, 2, 3, 12, 13, 14] {
576            is_ext[r] = Felt::ONE;
577        }
578
579        for r in 4..=10 {
580            is_packed_int[r] = Felt::ONE;
581        }
582
583        is_int_ext[11] = Felt::ONE;
584
585        // -------------------------------------------------------------------------
586        // Shared round constants (12 columns)
587        // -------------------------------------------------------------------------
588        // On external rows (0-3, 11-14): hold per-lane external round constants.
589        // On packed-internal rows (4-10): ark[0..2] hold 3 internal round constants,
590        //   ark[3..12] are zero.
591        // On boundary (row 15): all zero.
592        let ark = core::array::from_fn(|lane| {
593            let mut col = vec![Felt::ZERO; HASH_CYCLE_LEN];
594
595            // Row 0 (init+ext1): first initial external round constants
596            col[0] = Hasher::ARK_EXT_INITIAL[0][lane];
597
598            // Rows 1-3 (ext2, ext3, ext4): remaining initial external round constants
599            for r in 1..=3 {
600                col[r] = Hasher::ARK_EXT_INITIAL[r][lane];
601            }
602
603            // Rows 4-10 (packed internal): internal constants in lanes 0-2 only
604            if lane < 3 {
605                for triple in 0..7_usize {
606                    let row = 4 + triple;
607                    let ark_idx = triple * 3 + lane;
608                    col[row] = Hasher::ARK_INT[ark_idx];
609                }
610            }
611
612            // Row 11 (int22+ext5): terminal external round 0 constants
613            // (internal constant ARK_INT[21] is hardcoded in the constraint)
614            col[11] = Hasher::ARK_EXT_TERMINAL[0][lane];
615
616            // Rows 12-14 (ext6, ext7, ext8): remaining terminal external round constants
617            for r in 12..=14 {
618                col[r] = Hasher::ARK_EXT_TERMINAL[r - 11][lane];
619            }
620
621            col
622        });
623
624        Self {
625            is_init_ext,
626            is_ext,
627            is_packed_int,
628            is_int_ext,
629            ark,
630        }
631    }
632}
633
634impl Default for BitwisePeriodicCols<Vec<Felt>> {
635    fn default() -> Self {
636        Self::new()
637    }
638}
639
640impl BitwisePeriodicCols<Vec<Felt>> {
641    /// Generate periodic columns for the bitwise chiplet.
642    pub fn new() -> Self {
643        let k_first = vec![
644            Felt::ONE,
645            Felt::ZERO,
646            Felt::ZERO,
647            Felt::ZERO,
648            Felt::ZERO,
649            Felt::ZERO,
650            Felt::ZERO,
651            Felt::ZERO,
652        ];
653
654        let k_transition = vec![
655            Felt::ONE,
656            Felt::ONE,
657            Felt::ONE,
658            Felt::ONE,
659            Felt::ONE,
660            Felt::ONE,
661            Felt::ONE,
662            Felt::ZERO,
663        ];
664
665        Self { k_first, k_transition }
666    }
667}
668
669impl PeriodicCols<Vec<Felt>> {
670    /// Generate all chiplet periodic columns as a flat `Vec<Vec<Felt>>`.
671    pub fn periodic_columns() -> Vec<Vec<Felt>> {
672        let HasherPeriodicCols {
673            is_init_ext,
674            is_ext,
675            is_packed_int,
676            is_int_ext,
677            ark: [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11],
678        } = HasherPeriodicCols::new();
679
680        let BitwisePeriodicCols { k_first, k_transition } = BitwisePeriodicCols::new();
681
682        vec![
683            is_init_ext,
684            is_ext,
685            is_packed_int,
686            is_int_ext,
687            a0,
688            a1,
689            a2,
690            a3,
691            a4,
692            a5,
693            a6,
694            a7,
695            a8,
696            a9,
697            a10,
698            a11,
699            k_first,
700            k_transition,
701        ]
702    }
703}
704
705/// Total number of periodic columns across all chiplets.
706pub const NUM_PERIODIC_COLUMNS: usize = size_of::<PeriodicCols<u8>>();
707
708impl<T> Borrow<PeriodicCols<T>> for [T] {
709    fn borrow(&self) -> &PeriodicCols<T> {
710        debug_assert_eq!(self.len(), NUM_PERIODIC_COLUMNS);
711        let (prefix, cols, suffix) = unsafe { self.align_to::<PeriodicCols<T>>() };
712        debug_assert!(prefix.is_empty() && suffix.is_empty() && cols.len() == 1);
713        &cols[0]
714    }
715}
716
717const _: () = {
718    assert!(size_of::<PeriodicCols<u8>>() == 18);
719    assert!(size_of::<HasherPeriodicCols<u8>>() == 16);
720    assert!(size_of::<BitwisePeriodicCols<u8>>() == 2);
721
722    // PermutationCols and ControllerCols overlay chiplets[0..19] (19 columns,
723    // excluding s_00/s_01 which are consumed by the chiplet selector system).
724    assert!(size_of::<PermutationCols<u8>>() == 19);
725    assert!(size_of::<ControllerCols<u8>>() == 19);
726};
727
728// BORROW IMPLS
729// ================================================================================================
730//
731// Each chiplet column struct can be borrowed zero-copy from a `[T]` slice of the matching
732// length. Mirrors the `Borrow<CoreCols<T>>` / `Borrow<ChipletCols<T>>` impls on the parent
733// `crate::constraints::columns` module.
734
735impl_borrow_for_chiplet_cols!(PermutationCols);
736impl_borrow_for_chiplet_cols!(ControllerCols);
737impl_borrow_for_chiplet_cols!(BitwiseCols);
738impl_borrow_for_chiplet_cols!(MemoryCols);
739impl_borrow_for_chiplet_cols!(AceCols);
740impl_borrow_for_chiplet_cols!(AceReadCols);
741impl_borrow_for_chiplet_cols!(AceEvalCols);
742impl_borrow_for_chiplet_cols!(KernelRomCols);
743
744#[cfg(test)]
745mod tests {
746    use super::*;
747
748    #[test]
749    fn periodic_columns_dimensions() {
750        let cols = PeriodicCols::periodic_columns();
751        assert_eq!(cols.len(), NUM_PERIODIC_COLUMNS);
752
753        let (hasher_cols, bitwise_cols) = cols.split_at(size_of::<HasherPeriodicCols<u8>>());
754        for col in hasher_cols {
755            assert_eq!(col.len(), HASH_CYCLE_LEN);
756        }
757        for col in bitwise_cols {
758            assert_eq!(col.len(), 8);
759        }
760    }
761
762    #[test]
763    fn hasher_step_selectors_are_exclusive() {
764        let h = HasherPeriodicCols::new();
765        for row in 0..HASH_CYCLE_LEN {
766            let init_ext = h.is_init_ext[row];
767            let ext = h.is_ext[row];
768            let packed_int = h.is_packed_int[row];
769            let int_ext = h.is_int_ext[row];
770
771            // Each selector is binary.
772            assert_eq!(init_ext * (init_ext - Felt::ONE), Felt::ZERO);
773            assert_eq!(ext * (ext - Felt::ONE), Felt::ZERO);
774            assert_eq!(packed_int * (packed_int - Felt::ONE), Felt::ZERO);
775            assert_eq!(int_ext * (int_ext - Felt::ONE), Felt::ZERO);
776
777            // At most one selector is active per row.
778            let sum = init_ext + ext + packed_int + int_ext;
779            assert!(sum == Felt::ZERO || sum == Felt::ONE, "row {row}: sum = {sum}");
780        }
781    }
782
783    #[test]
784    fn external_round_constants_correct() {
785        let h = HasherPeriodicCols::new();
786
787        // Row 0: ARK_EXT_INITIAL[0]
788        for lane in 0..STATE_WIDTH {
789            assert_eq!(h.ark[lane][0], Hasher::ARK_EXT_INITIAL[0][lane]);
790        }
791
792        // Rows 1-3: ARK_EXT_INITIAL[1..3]
793        for r in 1..=3 {
794            for lane in 0..STATE_WIDTH {
795                assert_eq!(h.ark[lane][r], Hasher::ARK_EXT_INITIAL[r][lane]);
796            }
797        }
798
799        // Row 11: ARK_EXT_TERMINAL[0]
800        for lane in 0..STATE_WIDTH {
801            assert_eq!(h.ark[lane][11], Hasher::ARK_EXT_TERMINAL[0][lane]);
802        }
803
804        // Rows 12-14: ARK_EXT_TERMINAL[1..3]
805        for r in 12..=14 {
806            for lane in 0..STATE_WIDTH {
807                assert_eq!(h.ark[lane][r], Hasher::ARK_EXT_TERMINAL[r - 11][lane]);
808            }
809        }
810    }
811
812    #[test]
813    fn internal_round_constants_correct() {
814        let h = HasherPeriodicCols::new();
815
816        // Rows 4-10: packed internal round constants in ark[0..2]
817        for triple in 0..7_usize {
818            let row = 4 + triple;
819            for k in 0..3 {
820                let ark_idx = triple * 3 + k;
821                assert_eq!(
822                    h.ark[k][row],
823                    Hasher::ARK_INT[ark_idx],
824                    "mismatch at row {row}, int constant {k} (ARK_INT[{ark_idx}])"
825                );
826            }
827            // ark[3..12] must be zero on packed-internal rows
828            for lane in 3..STATE_WIDTH {
829                assert_eq!(
830                    h.ark[lane][row],
831                    Felt::ZERO,
832                    "ark[{lane}] nonzero at packed-int row {row}"
833                );
834            }
835        }
836    }
837
838    #[test]
839    fn boundary_row_all_zero() {
840        let h = HasherPeriodicCols::new();
841        for (lane, col) in h.ark.iter().enumerate() {
842            assert_eq!(col[15], Felt::ZERO, "ark column {lane} nonzero at row 15");
843        }
844    }
845}