Skip to main content

miden_ace_codegen/layout/
policy.rs

1use super::{
2    InputCounts, InputLayout, InputRegion, LayoutRegions, StarkVarIndices, plan::MultiAirVarIndices,
3};
4use crate::{EXT_DEGREE, randomness};
5
6#[derive(Clone, Copy)]
7enum Alignment {
8    Unaligned = 1,
9    Word = 2,
10    DoubleWord = 4,
11    QuadWord = 8,
12}
13
14#[derive(Clone, Copy)]
15struct LayoutPolicy {
16    public_values: Alignment,
17    randomness: Alignment,
18    main: Alignment,
19    aux: Alignment,
20    quotient: Alignment,
21    aux_bus_boundary: Alignment,
22    stark_vars: Alignment,
23    end_align: Option<Alignment>,
24}
25
26impl LayoutPolicy {
27    fn native() -> Self {
28        Self {
29            public_values: Alignment::Unaligned,
30            randomness: Alignment::Unaligned,
31            main: Alignment::Unaligned,
32            aux: Alignment::Unaligned,
33            quotient: Alignment::Unaligned,
34            aux_bus_boundary: Alignment::Unaligned,
35            stark_vars: Alignment::Unaligned,
36            end_align: None,
37        }
38    }
39
40    fn masm() -> Self {
41        Self {
42            public_values: Alignment::QuadWord,
43            randomness: Alignment::Word,
44            main: Alignment::DoubleWord,
45            aux: Alignment::DoubleWord,
46            quotient: Alignment::DoubleWord,
47            aux_bus_boundary: Alignment::Word,
48            stark_vars: Alignment::Word,
49            end_align: Some(Alignment::Word),
50        }
51    }
52}
53
54struct LayoutBuilder {
55    offset: usize,
56}
57
58impl LayoutBuilder {
59    fn new() -> Self {
60        Self { offset: 0 }
61    }
62
63    fn align(&mut self, alignment: Alignment) {
64        self.offset = self.offset.next_multiple_of(alignment as usize);
65    }
66
67    fn alloc(&mut self, width: usize, alignment: Alignment) -> InputRegion {
68        self.align(alignment);
69        let region = InputRegion { offset: self.offset, width };
70        self.offset += width;
71        region
72    }
73}
74
75impl InputLayout {
76    /// Build a native layout (no alignment/padding).
77    pub fn new(counts: InputCounts) -> Self {
78        Self::build_with_policy(counts, LayoutPolicy::native(), 1)
79    }
80
81    /// Build a MASM-compatible layout (alignment/padding enforced).
82    pub fn new_masm(counts: InputCounts) -> Self {
83        Self::build_with_policy(counts, LayoutPolicy::masm(), 1)
84    }
85
86    /// Build a native multi-AIR layout for a combined circuit over `num_airs` traces.
87    pub fn new_multi_air(counts: InputCounts, num_airs: usize) -> Self {
88        Self::build_with_policy(counts, LayoutPolicy::native(), num_airs)
89    }
90
91    /// Build a MASM-compatible multi-AIR layout (alignment/padding enforced; reserves
92    /// extra stark-vars slots for the per-AIR β coefficients and lifted selectors).
93    pub fn new_masm_multi_air(counts: InputCounts, num_airs: usize) -> Self {
94        Self::build_with_policy(counts, LayoutPolicy::masm(), num_airs)
95    }
96
97    fn build_with_policy(counts: InputCounts, policy: LayoutPolicy, num_airs: usize) -> Self {
98        assert!(num_airs >= 1, "layout requires at least one AIR");
99
100        // Number of EF slots in the stark-vars block. Every ACE input slot is an
101        // extension-field element (QuadFelt). Some stark vars are base-field values
102        // embedded as (val, 0); see the slot table below for which is which.
103        // A multi-AIR layout (num_airs >= 2) appends 4 more per AIR: one β coefficient
104        // and a (is_first, is_last, is_transition) lifted-selector triple.
105        const NUM_STARK_VARS_BASE: usize = 10;
106        let is_multi_air = num_airs >= 2;
107        let num_stark_vars = NUM_STARK_VARS_BASE + if is_multi_air { 4 * num_airs } else { 0 };
108
109        let mut builder = LayoutBuilder::new();
110
111        let public_values = builder.alloc(counts.num_public, policy.public_values);
112        /// Number of randomness inputs (alpha + beta).
113        const NUM_RANDOMNESS_INPUTS: usize = 2;
114        let randomness = builder.alloc(NUM_RANDOMNESS_INPUTS, policy.randomness);
115        let (aux_rand_alpha, aux_rand_beta) = randomness::aux_rand_indices(randomness);
116        let main_curr = builder.alloc(counts.width, policy.main);
117        let aux_coord_width = counts.aux_width * EXT_DEGREE;
118        let aux_curr = builder.alloc(aux_coord_width, policy.aux);
119        let quotient_curr = builder.alloc(counts.num_quotient_chunks * EXT_DEGREE, policy.quotient);
120        let main_next = builder.alloc(counts.width, policy.main);
121        let aux_next = builder.alloc(aux_coord_width, policy.aux);
122        let quotient_next = builder.alloc(counts.num_quotient_chunks * EXT_DEGREE, policy.quotient);
123        let aux_bus_boundary = builder.alloc(counts.num_aux_boundary, policy.aux_bus_boundary);
124
125        let stark_vars = builder.alloc(num_stark_vars, policy.stark_vars);
126
127        // Matches utils::set_up_auxiliary_inputs_ace layout (EF slots).
128        //
129        // Extension-field values are grouped first (slots 0-6), then base-field
130        // values stored as (val, 0) in EF slots (slots 7-9).
131        //
132        //  Slot  Value               Field
133        //  ----  ------------------  -----
134        //   0    alpha               EF      Composition challenge (Horner multiplier)
135        //   1    z^N                 EF      Trace-length power (quotient deltas + vanishing
136        //   2    z_k                 EF      Periodic column eval point
137        //   3    is_first            EF      Precomputed: (z^N - 1) / (z - 1)
138        //   4    is_last             EF      Precomputed: (z^N - 1) / (z - g^{-1})
139        //   5    is_transition       EF      Precomputed: z - g^{-1}
140        //   6    reserved            EF      Alignment padding (zero)
141        //   7    weight0             base    First barycentric weight
142        //   8    f                   base    Chunk shift ratio h^N
143        //   9    s0                  base    First coset shift offset^N
144        let b = stark_vars.offset;
145        let alpha = b;
146        let z_pow_n = b + 1;
147        let z_k = b + 2;
148        let is_first = b + 3;
149        let is_last = b + 4;
150        let is_transition = b + 5;
151        let reserved = b + 6;
152        let weight0 = b + 7;
153        let f = b + 8;
154        let s0 = b + 9;
155        // Multi-AIR block: β coefficients at b+10..b+10+num_airs, then one selector
156        // triple per AIR. For num_airs = 2 this is slots 10-11 (betas) and 12-17
157        // (selectors), matching `set_up_auxiliary_inputs_ace` in MASM.
158        let multi_air = is_multi_air.then_some(MultiAirVarIndices { base: b + 10, num_airs });
159
160        if let Some(end_align) = policy.end_align {
161            builder.align(end_align);
162        }
163
164        Self {
165            regions: LayoutRegions {
166                public_values,
167                randomness,
168                main_curr,
169                aux_curr,
170                quotient_curr,
171                main_next,
172                aux_next,
173                quotient_next,
174                aux_bus_boundary,
175                stark_vars,
176            },
177            aux_rand_alpha,
178            aux_rand_beta,
179            stark: StarkVarIndices {
180                alpha,
181                z_pow_n,
182                z_k,
183                is_first,
184                is_last,
185                is_transition,
186                reserved,
187                weight0,
188                f,
189                s0,
190                multi_air,
191            },
192            total_inputs: builder.offset,
193            counts,
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::super::{InputCounts, InputKey, InputLayout};
201
202    #[test]
203    fn multi_air_layout_generalizes_over_num_airs() {
204        let counts = InputCounts {
205            width: 1,
206            aux_width: 1,
207            num_aux_boundary: 3,
208            num_public: 8,
209            num_randomness: 2,
210            num_periodic: 0,
211            num_quotient_chunks: 1,
212        };
213
214        // 3-AIR layout: betas first (instance order), then one selector triple per AIR.
215        let layout = InputLayout::new_multi_air(counts, 3);
216        let base = layout.index(InputKey::MultiAirBeta(0)).unwrap();
217        assert_eq!(layout.index(InputKey::MultiAirBeta(2)), Some(base + 2));
218        assert_eq!(layout.index(InputKey::IsFirstAir(0)), Some(base + 3));
219        assert_eq!(layout.index(InputKey::IsTransitionAir(2)), Some(base + 3 + 3 * 2 + 2));
220        assert_eq!(layout.index(InputKey::MultiAirBeta(3)), None, "AIR index out of range");
221        assert_eq!(layout.index(InputKey::IsFirstAir(3)), None, "AIR index out of range");
222
223        // 2-AIR layout keeps the slot positions the MASM verifier writes:
224        // betas at base..base+1, selector triples at base+2..base+7.
225        let layout2 = InputLayout::new_multi_air(counts, 2);
226        let base2 = layout2.index(InputKey::MultiAirBeta(0)).unwrap();
227        assert_eq!(layout2.index(InputKey::MultiAirBeta(1)), Some(base2 + 1));
228        assert_eq!(layout2.index(InputKey::IsFirstAir(0)), Some(base2 + 2));
229        assert_eq!(layout2.index(InputKey::IsTransitionAir(1)), Some(base2 + 7));
230    }
231}