openvm_recursion_circuit/subairs/nested_for_loop/
air.rs

1use openvm_circuit_primitives::{StructReflection, StructReflectionHelper, SubAir};
2use openvm_recursion_circuit_derive::AlignedBorrow;
3use p3_air::AirBuilder;
4use p3_field::PrimeCharacteristicRing;
5
6/// A SubAir that constrains the `is_first` flags for nested for-loops.
7///
8/// Enabled rows must appear contiguously at the beginning of the trace (no interspersed padding),
9/// and the `is_enabled` flag is enforced to be boolean. Each tracked counter is also zeroed on
10/// the first enabled row of its scope.
11///
12/// Tracks `DEPTH_MINUS_ONE` loop counters (all loops except the innermost).
13#[derive(Default)]
14pub struct NestedForLoopSubAir<const DEPTH_MINUS_ONE: usize>;
15
16#[repr(C)]
17#[derive(AlignedBorrow, StructReflection, Copy, Clone, Debug)]
18pub struct NestedForLoopIoCols<T, const DEPTH_MINUS_ONE: usize> {
19    /// Whether the current row is enabled (i.e. not padding)
20    pub is_enabled: T,
21    /// Array of loop counters for all parent loops (excludes innermost loop).
22    /// For DEPTH=3 (i,j,k loops): contains [i, j].
23    /// The innermost loop counter is managed by the caller.
24    pub counter: [T; DEPTH_MINUS_ONE],
25    /// Array of flags indicating the first row of each loop iteration (excludes outermost loop).
26    /// For DEPTH=3 (i,j,k loops): contains [j_is_first, k_is_first].
27    /// The outermost loop's `is_first` is handled by `when_first_row()` and has no stored column.
28    pub is_first: [T; DEPTH_MINUS_ONE],
29}
30
31impl<T, const DEPTH_MINUS_ONE: usize> NestedForLoopIoCols<T, DEPTH_MINUS_ONE> {
32    pub fn map_into<S>(self) -> NestedForLoopIoCols<S, DEPTH_MINUS_ONE>
33    where
34        T: Into<S>,
35    {
36        NestedForLoopIoCols {
37            is_enabled: self.is_enabled.into(),
38            counter: self.counter.map(Into::into),
39            is_first: self.is_first.map(Into::into),
40        }
41    }
42}
43
44impl<AB: AirBuilder, const DEPTH_MINUS_ONE: usize> SubAir<AB>
45    for NestedForLoopSubAir<DEPTH_MINUS_ONE>
46{
47    type AirContext<'a>
48        = (
49        NestedForLoopIoCols<AB::Expr, DEPTH_MINUS_ONE>,
50        NestedForLoopIoCols<AB::Expr, DEPTH_MINUS_ONE>,
51    )
52    where
53        AB: 'a,
54        AB::Var: 'a,
55        AB::Expr: 'a;
56
57    fn eval<'a>(&'a self, builder: &'a mut AB, ctx: Self::AirContext<'a>)
58    where
59        AB::Var: 'a,
60        AB::Expr: 'a,
61    {
62        let (local_io, next_io) = ctx;
63
64        // Enforce boolean enabled flag and forbid re-enabling after a disabled row.
65        builder.assert_bool(local_io.is_enabled.clone());
66        builder
67            .when_transition()
68            .when_ne(local_io.is_enabled.clone(), AB::Expr::ONE)
69            .assert_zero(next_io.is_enabled.clone());
70
71        for level in 0..DEPTH_MINUS_ONE {
72            let counter_diff = next_io.counter[level].clone() - local_io.counter[level].clone();
73            let local_is_first = local_io.is_first[level].clone();
74            let next_is_first = next_io.is_first[level].clone();
75
76            builder.assert_bool(local_is_first.clone());
77            builder.assert_bool(next_is_first.clone());
78            builder
79                .when(local_io.is_first[level].clone())
80                .assert_one(local_io.is_enabled.clone());
81
82            // First row constraint: mark the loop boundary and zero the counter for this scope.
83            let mut builder_when_first_row = if level == 0 {
84                builder.when_first_row()
85            } else {
86                let parent_level = level - 1;
87                let parent_is_first = local_io.is_first[parent_level].clone();
88                builder.when(parent_is_first)
89            };
90            self.eval_first_row(&mut builder_when_first_row, &local_io, local_is_first);
91            builder_when_first_row.assert_zero(local_io.counter[level].clone());
92
93            // Transition constraints
94            let mut builder_when_transition = if level == 0 {
95                builder.when_transition()
96            } else {
97                let parent_level = level - 1;
98                let parent_next_is_first = next_io.is_first[parent_level].clone();
99                let parent_is_transition: AB::Expr =
100                    Self::local_is_transition(next_io.is_enabled.clone(), parent_next_is_first);
101
102                builder.when(parent_is_transition)
103            };
104
105            self.eval_transition(
106                &mut builder_when_transition,
107                &local_io,
108                &next_io,
109                next_is_first.clone(),
110                counter_diff.clone(),
111            );
112
113            // At Loop Boundaries (Δcounter ≠ 0)
114            builder
115                .when(next_io.is_enabled.clone())
116                .when(counter_diff)
117                .assert_one(next_is_first.clone());
118        }
119    }
120}
121
122impl<const DEPTH_MINUS_ONE: usize> NestedForLoopSubAir<DEPTH_MINUS_ONE> {
123    /// Evaluates first row constraint for a loop level.
124    ///
125    /// Constraint: First row enabled sets `is_first`
126    fn eval_first_row<AB: AirBuilder>(
127        &self,
128        builder_first_row: &mut AB,
129        local_io: &NestedForLoopIoCols<AB::Expr, DEPTH_MINUS_ONE>,
130        local_is_first: AB::Expr,
131    ) {
132        builder_first_row
133            .when(local_io.is_enabled.clone())
134            .assert_one(local_is_first);
135    }
136
137    /// Evaluates transition constraints for a loop level.
138    ///
139    /// Constraints:
140    /// 1. When the next row remains enabled, `counter[level]` increments by 0 or 1
141    /// 2. Within Loop: `is_first` not set within iteration
142    fn eval_transition<AB: AirBuilder>(
143        &self,
144        builder_transition: &mut AB,
145        local_io: &NestedForLoopIoCols<AB::Expr, DEPTH_MINUS_ONE>,
146        next_io: &NestedForLoopIoCols<AB::Expr, DEPTH_MINUS_ONE>,
147        next_is_first: AB::Expr,
148        counter_diff: AB::Expr,
149    ) {
150        // 1. Base Constraint: `counter[level]` increments by 0 or 1 while enabled
151        builder_transition
152            .when(next_io.is_enabled.clone())
153            .assert_bool(counter_diff.clone());
154
155        // 2. Within Loop (Δcounter ≠ 1): `is_first` not set within iteration
156        builder_transition
157            .when(local_io.is_enabled.clone())
158            .when_ne(counter_diff, AB::Expr::ONE)
159            .assert_zero(next_is_first);
160    }
161
162    /// Returns an expression for `is_transition` on enabled rows.
163    ///
164    /// True when:
165    /// - The next row is enabled, AND
166    /// - The next row does not have `is_first` set (continuation within same loop iteration)
167    pub fn local_is_transition<FA>(
168        next_is_enabled: impl Into<FA>,
169        next_is_first: impl Into<FA>,
170    ) -> FA
171    where
172        FA: PrimeCharacteristicRing,
173    {
174        next_is_enabled.into() - next_is_first.into()
175    }
176
177    /// Returns an expression for `is_last` on enabled rows.
178    ///
179    /// Equivalent to `!is_transition`.
180    /// True when either:
181    /// - The next row is disabled, OR
182    /// - The next row is enabled and has `is_first` set (boundary between loop iterations)
183    pub fn local_is_last<FA>(
184        local_is_enabled: impl Into<FA>,
185        next_is_enabled: impl Into<FA>,
186        next_is_first: impl Into<FA>,
187    ) -> FA
188    where
189        FA: PrimeCharacteristicRing,
190    {
191        local_is_enabled.into() - next_is_enabled.into() + next_is_first.into()
192    }
193}