pub mod program;
use alloc::{vec, vec::Vec};
use core::{array, ops::Range};
use miden_core::{
Felt,
field::{Algebra, PrimeCharacteristicRing, QuadFelt},
utils::RowMajorMatrix,
};
use miden_lifted_air::{AirBuilder, BaseAir, LiftedAir, LiftedAirBuilder};
pub use program::{NUM_PERIODIC_COLS, Op, ROUND_PERIOD, Slot, round_program, slots};
use crate::{
hash::{keccak::reference::KECCAK_RC, memory64::Memory64Msg},
logup::{
CyclicConstraintLookupBuilder, Deg, LookupAir, LookupBatch, LookupBuilder, LookupColumn,
LookupGroup, NUM_PUBLIC_VALUES, NUM_RANDOMNESS, NUM_SIGMA_VALUES, build_logup_aux_trace,
frac_col,
},
primitives::byte_pair_lut::{
BytePairLutMsg, BytePairLutRequires, BytePairOp, Range16Msg, require_logic64,
},
relations::{MAX_MESSAGE_WIDTH, NUM_BUS_IDS},
utils::{current_main, halves_le, next_main, pack_le},
};
pub const COL_IP: usize = 0;
pub const A_BYTES_RANGE: Range<usize> = 1..9;
pub const B_BYTES_RANGE: Range<usize> = 9..17;
pub const R_BYTES_RANGE: Range<usize> = 17..25;
pub const ROT_LIMBS_RANGE: Range<usize> = 25..33;
pub const COL_ACT: usize = 33;
pub const LANE_WIDTH: usize = 34;
pub const NUM_LANES: usize = 2;
pub const NUM_MAIN_COLS: usize = LANE_WIDTH * NUM_LANES;
#[inline]
pub fn lane_base(lane: usize) -> usize {
lane * LANE_WIDTH
}
fn interleave_lanes(lane_cells: &[Vec<Felt>; NUM_LANES], height: usize) -> RowMajorMatrix<Felt> {
let mut trace = vec![Felt::ZERO; height * NUM_MAIN_COLS];
for r in 0..height {
let row_start = r * NUM_MAIN_COLS;
for (lane, cells) in lane_cells.iter().enumerate() {
let base = lane_base(lane);
let src = &cells[r * LANE_WIDTH..(r + 1) * LANE_WIDTH];
trace[row_start + base..row_start + base + LANE_WIDTH].copy_from_slice(src);
}
}
RowMajorMatrix::new(trace, NUM_MAIN_COLS)
}
pub const NUM_AUX_COLS: usize = 10 * NUM_LANES;
pub use program::{
COL_BACK_A as PCOL_BACK_A, COL_BACK_B as PCOL_BACK_B, COL_DST_MULT as PCOL_DST_MULT,
COL_IS_ANDNOT as PCOL_IS_ANDNOT, COL_IS_ROL as PCOL_IS_ROL, COL_IS_XOR as PCOL_IS_XOR,
COL_IS_XORROL as PCOL_IS_XORROL, COL_K as PCOL_K, COL_P_LAST as PCOL_P_LAST,
COL_SWAP as PCOL_SWAP,
};
#[derive(Debug, Default, Clone, Copy)]
pub struct KeccakRoundAir;
impl BaseAir<Felt> for KeccakRoundAir {
fn width(&self) -> usize {
NUM_MAIN_COLS
}
fn num_public_values(&self) -> usize {
NUM_PUBLIC_VALUES
}
fn periodic_columns(&self) -> Vec<Vec<Felt>> {
round_program().to_vec()
}
}
impl LiftedAir<Felt, QuadFelt> for KeccakRoundAir {
fn num_randomness(&self) -> usize {
NUM_RANDOMNESS
}
fn aux_width(&self) -> usize {
NUM_AUX_COLS
}
fn num_aux_values(&self) -> usize {
NUM_SIGMA_VALUES
}
fn build_aux_trace(
&self,
main: &RowMajorMatrix<Felt>,
_air_inputs: &[Felt],
_aux_inputs: &[Felt],
challenges: &[QuadFelt],
) -> (RowMajorMatrix<QuadFelt>, Vec<QuadFelt>) {
build_aux(main, challenges)
}
fn eval<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
let local: [AB::Var; NUM_MAIN_COLS] = current_main(builder.main(), 0);
let next_window: [AB::Var; NUM_MAIN_COLS] = next_main(builder.main(), 0);
let periodic = builder.periodic_values();
let p_last: AB::Expr = periodic[PCOL_P_LAST].into();
let is_xor: AB::Expr = periodic[PCOL_IS_XOR].into();
let is_andnot: AB::Expr = periodic[PCOL_IS_ANDNOT].into();
let is_rol: AB::Expr = periodic[PCOL_IS_ROL].into();
let k: AB::Expr = periodic[PCOL_K].into();
let two_32 = AB::Expr::from(Felt::new(1u64 << 32).expect("2^32 fits"));
for lane in 0..NUM_LANES {
let base = lane_base(lane);
let ip = local[base + COL_IP];
let next_ip = next_window[base + COL_IP];
let act: AB::Expr = local[base + COL_ACT].into();
let next_act = next_window[base + COL_ACT];
if lane == 0 {
builder.when_first_row().assert_eq(ip, AB::Expr::from(Felt::from(25u8)));
}
builder
.when_transition()
.assert_zero(AB::Expr::from(next_ip) - AB::Expr::from(ip) - AB::Expr::ONE);
builder.assert_bool(local[base + COL_ACT]);
builder.assert_zero(
(AB::Expr::ONE - p_last.clone()) * (AB::Expr::from(next_act) - act.clone()),
);
let logic_active = is_xor.clone() + is_andnot.clone();
let no_logic = AB::Expr::ONE - logic_active;
for i in 0..8 {
let a_byte = local[base + A_BYTES_RANGE.start + i];
let r_byte = local[base + R_BYTES_RANGE.start + i];
builder.assert_zero(
no_logic.clone() * (AB::Expr::from(r_byte) - AB::Expr::from(a_byte)),
);
}
let r_bytes: [AB::Var; 8] = array::from_fn(|i| local[base + R_BYTES_RANGE.start + i]);
let rot_limbs: [AB::Var; 8] =
array::from_fn(|i| local[base + ROT_LIMBS_RANGE.start + i]);
let [r_lo, r_hi] = halves_le(&r_bytes, 256);
let lo_decomp: AB::Expr = pack_le(&rot_limbs[0..4], 1u64 << 16);
let hi_decomp: AB::Expr = pack_le(&rot_limbs[4..8], 1u64 << 16);
let rol_gate = act.clone() * is_rol.clone();
builder
.assert_zero(rol_gate.clone() * ((r_lo + two_32.clone()) * k.clone() - lo_decomp));
builder.assert_zero(rol_gate * ((r_hi + two_32.clone()) * k.clone() - hi_decomp));
}
let mut lb =
CyclicConstraintLookupBuilder::new(builder, self, self.preprocessed_width() > 0);
<Self as LookupAir<_>>::eval(self, &mut lb);
}
}
fn rotated_halves<E: Algebra<Felt>, V: Copy + Into<E>>(
rot_limbs: &[V; 8],
k: E,
swap: E,
) -> [E; 2] {
let two_16 = E::from(Felt::from(1u32 << 16));
let limb: [E; 8] = array::from_fn(|i| rot_limbs[i].into());
let c0 = limb[0].clone() + limb[6].clone();
let c1 = limb[1].clone() + limb[7].clone();
let c2 = limb[2].clone() + limb[4].clone();
let c3 = limb[3].clone() + limb[5].clone();
let lo = c0 + c1 * two_16.clone() - k.clone();
let hi = c2 + c3 * two_16 - k;
let lo_final = lo.clone() + swap.clone() * (hi.clone() - lo.clone());
let hi_final = hi.clone() + swap * (lo - hi);
[lo_final, hi_final]
}
fn memory_provide_c<E: Algebra<Felt>, V: Copy + Into<E>>(
r_bytes: &[V; 8],
rot_limbs: &[V; 8],
k: E,
swap: E,
is_rol: E,
) -> [E; 2] {
let [r_lo, r_hi] = halves_le(r_bytes, 256);
let [rot_lo, rot_hi] = rotated_halves(rot_limbs, k, swap);
let lo = r_lo.clone() + is_rol.clone() * (rot_lo - r_lo);
let hi = r_hi.clone() + is_rol * (rot_hi - r_hi);
[lo, hi]
}
const COLUMN_SHAPE: [usize; NUM_AUX_COLS] = build_column_shape();
const fn build_column_shape() -> [usize; NUM_AUX_COLS] {
let mut shape = [2usize; NUM_AUX_COLS];
let mut lane = 0;
while lane < NUM_LANES {
shape[lane * 10] = 1;
lane += 1;
}
shape
}
impl<LB> LookupAir<LB> for KeccakRoundAir
where
LB: LookupBuilder<F = Felt>,
{
fn num_columns(&self) -> usize {
NUM_AUX_COLS
}
fn column_shape(&self) -> &[usize] {
&COLUMN_SHAPE
}
fn max_message_width(&self) -> usize {
MAX_MESSAGE_WIDTH
}
fn num_bus_ids(&self) -> usize {
NUM_BUS_IDS
}
fn eval(&self, builder: &mut LB) {
let local: [LB::Var; NUM_MAIN_COLS] = current_main(builder.main(), 0);
let periodic = builder.periodic_values();
let is_xor: LB::Expr = periodic[PCOL_IS_XOR].into();
let is_andnot: LB::Expr = periodic[PCOL_IS_ANDNOT].into();
let is_rol: LB::Expr = periodic[PCOL_IS_ROL].into();
let is_xorrol: LB::Expr = periodic[PCOL_IS_XORROL].into();
let back_a: LB::Expr = periodic[PCOL_BACK_A].into();
let back_b: LB::Expr = periodic[PCOL_BACK_B].into();
let k: LB::Expr = periodic[PCOL_K].into();
let dst_mult: LB::Expr = periodic[PCOL_DST_MULT].into();
let swap: LB::Expr = periodic[PCOL_SWAP].into();
let bpl_op = LB::Expr::ONE - is_andnot.clone();
let logic_active = is_xor.clone() + is_andnot.clone();
let interaction_deg = Deg { v: 1, u: 1 };
let triple_deg = Deg { v: 3, u: 3 };
let pair_deg = Deg { v: 2, u: 2 };
let dst_deg = Deg { v: 5, u: 2 };
for lane in 0..NUM_LANES {
let base = lane_base(lane);
let ip: LB::Expr = local[base + COL_IP].into();
let act: LB::Expr = local[base + COL_ACT].into();
let a_bytes: [LB::Var; 8] = array::from_fn(|i| local[base + A_BYTES_RANGE.start + i]);
let b_bytes: [LB::Var; 8] = array::from_fn(|i| local[base + B_BYTES_RANGE.start + i]);
let r_bytes: [LB::Var; 8] = array::from_fn(|i| local[base + R_BYTES_RANGE.start + i]);
let rot_limbs: [LB::Var; 8] =
array::from_fn(|i| local[base + ROT_LIMBS_RANGE.start + i]);
let is_active = act.clone()
* (is_xor.clone() + is_andnot.clone() + is_rol.clone() - is_xorrol.clone());
let reads_b = act.clone() * (is_xor.clone() + is_andnot.clone());
let rol_act = act.clone() * is_rol.clone();
let dst_mult_act = act.clone() * dst_mult.clone();
let [c_lo, c_hi] =
memory_provide_c(&r_bytes, &rot_limbs, k.clone(), swap.clone(), is_rol.clone());
let neg_dst_mult: LB::Expr = LB::Expr::ZERO - dst_mult_act;
frac_col!(
builder,
"memory64",
dst_deg,
(
"dst",
neg_dst_mult,
Memory64Msg { addr: ip.clone(), lo: c_lo, hi: c_hi },
interaction_deg
),
);
let [a_lo, a_hi] = halves_le(&a_bytes, 256);
let [b_lo, b_hi] = halves_le(&b_bytes, 256);
frac_col!(
builder,
"memory64",
triple_deg,
(
"src_a",
is_active.clone(),
Memory64Msg {
addr: ip.clone() - back_a.clone(),
lo: a_lo,
hi: a_hi
},
interaction_deg
),
(
"src_b",
reads_b.clone(),
Memory64Msg {
addr: ip - back_b.clone(),
lo: b_lo,
hi: b_hi
},
interaction_deg
),
);
for pair in 0..4 {
let i0 = pair * 2;
let i1 = i0 + 1;
let gated_b0 = logic_active.clone() * LB::Expr::from(b_bytes[i0]);
let gated_b1 = logic_active.clone() * LB::Expr::from(b_bytes[i1]);
frac_col!(
builder,
"byte-pair-lut",
pair_deg,
(
"byte-req",
is_active.clone(),
BytePairLutMsg {
op: bpl_op.clone(),
a: a_bytes[i0].into(),
b: gated_b0,
c: r_bytes[i0].into()
},
interaction_deg
),
(
"byte-req",
is_active.clone(),
BytePairLutMsg {
op: bpl_op.clone(),
a: a_bytes[i1].into(),
b: gated_b1,
c: r_bytes[i1].into()
},
interaction_deg
),
);
}
for pair in 0..4 {
let i0 = pair * 2;
let i1 = i0 + 1;
frac_col!(
builder,
"range16",
pair_deg,
(
"limb",
rol_act.clone(),
Range16Msg { w: rot_limbs[i0].into() },
interaction_deg
),
(
"limb",
rol_act.clone(),
Range16Msg { w: rot_limbs[i1].into() },
interaction_deg
),
);
}
}
}
}
pub const IP_BOUNDARY: u64 = 25;
pub const NUM_ROUNDS: usize = 24;
pub const PERM_CYCLE: usize = (NUM_ROUNDS + 1) * ROUND_PERIOD;
fn simulate_logic(op: Op, a: u64, b: u64) -> u64 {
match op {
Op::Nop | Op::Rol(_) => a,
Op::Xor | Op::XorRol(_) => a ^ b,
Op::Andnot => (!a) & b,
}
}
fn simulate_rotate(op: Op, r: u64) -> u64 {
match op {
Op::Rol(s) | Op::XorRol(s) => r.rotate_left(s),
_ => r,
}
}
fn bytes_le(x: u64) -> [Felt; 8] {
x.to_le_bytes().map(Felt::from)
}
fn rot_limbs_for(r: u64, shift: u32) -> [u16; 8] {
let k = 1u64 << shift;
let r_lo = r & 0xffff_ffff;
let r_hi = r >> 32;
let lo_offset_k = (r_lo + (1u64 << 32)).wrapping_mul(k);
let hi_offset_k = (r_hi + (1u64 << 32)).wrapping_mul(k);
let lo_limbs = u64_as_four_u16_limbs(lo_offset_k);
let hi_limbs = u64_as_four_u16_limbs(hi_offset_k);
[
lo_limbs[0],
lo_limbs[1],
lo_limbs[2],
lo_limbs[3],
hi_limbs[0],
hi_limbs[1],
hi_limbs[2],
hi_limbs[3],
]
}
fn u64_as_four_u16_limbs(x: u64) -> [u16; 4] {
[
(x & 0xffff) as u16,
((x >> 16) & 0xffff) as u16,
((x >> 32) & 0xffff) as u16,
((x >> 48) & 0xffff) as u16,
]
}
fn push_row(
trace: &mut Vec<Felt>,
bpl_req: &mut BytePairLutRequires,
ip: u64,
spec: &Slot,
a: u64,
b: u64,
act: bool,
) {
let is_andnot = matches!(spec.op, Op::Andnot);
let logic_active = matches!(spec.op, Op::Xor | Op::Andnot | Op::XorRol(_));
let reads_a = !matches!(spec.op, Op::Nop);
let is_rol = matches!(spec.op, Op::Rol(_) | Op::XorRol(_));
let b_eff = if logic_active { b } else { 0 };
let r = simulate_logic(spec.op, a, b_eff);
if act && reads_a {
let bpl_op = if is_andnot { BytePairOp::AndNot } else { BytePairOp::Xor };
require_logic64(bpl_req, bpl_op, a, b_eff);
}
let mut rot_limbs = [0u16; 8];
if let Op::Rol(s) | Op::XorRol(s) = spec.op {
let (shift, _swap) = program::rol_decompose(s);
rot_limbs = rot_limbs_for(r, shift);
if act {
for limb in rot_limbs {
bpl_req.require_range16(limb);
}
}
}
trace.push(Felt::new(ip).expect("ip fits in canonical Goldilocks"));
trace.extend(bytes_le(a));
trace.extend(bytes_le(b_eff));
trace.extend(bytes_le(r));
trace.extend(rot_limbs.map(Felt::from));
trace.push(Felt::from(act as u8));
let _ = is_rol;
}
pub fn generate_trace_from_states(
states: &[[u64; 25]],
rcs: &[u64; NUM_ROUNDS],
) -> RowMajorMatrix<Felt> {
let mut scratch = BytePairLutRequires::new();
generate_trace_from_states_inner(states, rcs, &mut scratch)
}
fn generate_trace_from_states_inner(
states: &[[u64; 25]],
rcs: &[u64; NUM_ROUNDS],
bpl_req: &mut BytePairLutRequires,
) -> RowMajorMatrix<Felt> {
assert!(!states.is_empty(), "at least one perm required");
let num_perms = states.len();
let active_rows_per_cycle = NUM_ROUNDS * ROUND_PERIOD;
let perms_per_lane = num_perms.div_ceil(NUM_LANES);
let height = (perms_per_lane * PERM_CYCLE).next_power_of_two().max(2);
let program = slots();
let mem_size = IP_BOUNDARY as usize + NUM_LANES * perms_per_lane * PERM_CYCLE + 1;
let mut memory = vec![0u64; mem_size];
for (n, state) in states.iter().enumerate() {
let perm_base = (n * PERM_CYCLE) as u64;
for (idx, &lane) in state.iter().enumerate() {
memory[(perm_base + idx as u64) as usize] = lane;
}
for r in 0..NUM_ROUNDS {
memory[(IP_BOUNDARY + perm_base + (r * ROUND_PERIOD) as u64) as usize] = rcs[r];
}
}
let lane_cells: [Vec<Felt>; NUM_LANES] = array::from_fn(|lane| {
let base_perm = lane * perms_per_lane;
let lane_perms = num_perms.saturating_sub(base_perm).min(perms_per_lane);
let row_offset = base_perm * PERM_CYCLE;
let mut cells = Vec::with_capacity(height * LANE_WIDTH);
for r in 0..height {
let ip = IP_BOUNDARY + (row_offset + r) as u64;
let perm_in_lane = r / PERM_CYCLE;
let row_in_cycle = r % PERM_CYCLE;
if perm_in_lane >= lane_perms {
push_row(
&mut cells,
bpl_req,
ip,
&Slot {
op: Op::Nop,
back_a: 0,
back_b: 0,
dst_mult: 0,
},
0,
0,
false,
);
continue;
}
let spec = program[r % ROUND_PERIOD];
let act = row_in_cycle < active_rows_per_cycle;
let reads_a = !matches!(spec.op, Op::Nop);
let reads_b = matches!(spec.op, Op::Xor | Op::Andnot | Op::XorRol(_));
let a = if reads_a {
memory[ip.wrapping_sub(spec.back_a) as usize]
} else {
0
};
let b = if reads_b {
memory[ip.wrapping_sub(spec.back_b) as usize]
} else {
0
};
let r_val = simulate_logic(spec.op, a, b);
let c_val = simulate_rotate(spec.op, r_val);
if act && spec.dst_mult > 0 {
memory[ip as usize] = c_val;
}
push_row(&mut cells, bpl_req, ip, &spec, a, b, act);
}
cells
});
interleave_lanes(&lane_cells, height)
}
pub fn extract_outputs(states: &[[u64; 25]], rcs: &[u64; NUM_ROUNDS]) -> Vec<[u64; 25]> {
assert!(!states.is_empty(), "at least one perm required");
let num_perms = states.len();
let active_rows_per_cycle = NUM_ROUNDS * ROUND_PERIOD;
let total_rows = num_perms * PERM_CYCLE;
let program = slots();
let mut memory = vec![0u64; IP_BOUNDARY as usize + total_rows];
for (n, state) in states.iter().enumerate() {
let perm_base = (n * PERM_CYCLE) as u64;
for (idx, &lane) in state.iter().enumerate() {
memory[(perm_base + idx as u64) as usize] = lane;
}
for r in 0..NUM_ROUNDS {
memory[(IP_BOUNDARY + perm_base + (r * ROUND_PERIOD) as u64) as usize] = rcs[r];
}
}
for row in 0..total_rows {
let row_in_cycle = row % PERM_CYCLE;
if row_in_cycle >= active_rows_per_cycle {
continue;
}
let slot = row % ROUND_PERIOD;
let ip = IP_BOUNDARY + row as u64;
let spec = program[slot];
let reads_a = !matches!(spec.op, Op::Nop);
let reads_b = matches!(spec.op, Op::Xor | Op::Andnot | Op::XorRol(_));
let a = if reads_a {
memory[ip.wrapping_sub(spec.back_a) as usize]
} else {
0
};
let b = if reads_b {
memory[ip.wrapping_sub(spec.back_b) as usize]
} else {
0
};
let r = simulate_logic(spec.op, a, b);
let c = simulate_rotate(spec.op, r);
if spec.dst_mult > 0 {
memory[ip as usize] = c;
}
}
let mut outputs = Vec::with_capacity(num_perms);
for n in 0..num_perms {
let perm_base = (n * PERM_CYCLE) as u64;
let last_round_base = IP_BOUNDARY + perm_base + (23 * ROUND_PERIOD) as u64;
let mut out = [0u64; 25];
for (idx, out_limb) in out.iter_mut().enumerate() {
let slot = if idx == 0 {
program::SLOT_IOTA
} else {
program::SLOT_CHI_XOR_BEGIN + (idx - 1)
};
*out_limb = memory[(last_round_base + slot as u64) as usize];
}
outputs.push(out);
}
outputs
}
pub fn extract_output(state: &[u64; 25], rcs: &[u64; NUM_ROUNDS]) -> [u64; 25] {
extract_outputs(core::slice::from_ref(state), rcs)
.into_iter()
.next()
.expect("single-perm extract")
}
pub(crate) fn build_aux(
main: &RowMajorMatrix<Felt>,
challenges: &[QuadFelt],
) -> (RowMajorMatrix<QuadFelt>, Vec<QuadFelt>) {
build_logup_aux_trace(&KeccakRoundAir, main, challenges)
}
#[derive(Debug, Default, Clone)]
pub struct RoundRequires {
rounds: Vec<[u64; 25]>,
}
impl RoundRequires {
pub fn new() -> Self {
Self::default()
}
pub fn require_round(&mut self, state_in: [u64; 25]) {
self.rounds.push(state_in);
}
pub fn total_rounds(&self) -> u32 {
self.rounds.len() as u32
}
pub fn total_perms(&self) -> u32 {
self.total_rounds() / NUM_ROUNDS as u32
}
}
pub fn generate_trace(
requires: RoundRequires,
bpl_req: &mut BytePairLutRequires,
) -> RowMajorMatrix<Felt> {
assert!(
requires.rounds.len().is_multiple_of(NUM_ROUNDS),
"RoundRequires must hold a multiple of {NUM_ROUNDS} rounds (got {})",
requires.rounds.len(),
);
let num_perms = requires.total_perms() as usize;
let active_rows_per_cycle = NUM_ROUNDS * ROUND_PERIOD;
let perms_per_lane = num_perms.max(1).div_ceil(NUM_LANES);
let height = (perms_per_lane * PERM_CYCLE).next_power_of_two().max(2);
let program = slots();
let mem_size = IP_BOUNDARY as usize + NUM_LANES * perms_per_lane * PERM_CYCLE + 1;
let mut memory = vec![0u64; mem_size];
for n in 0..num_perms {
let perm_base = (n * PERM_CYCLE) as u64;
let round0_state = &requires.rounds[n * NUM_ROUNDS];
for (idx, &lane) in round0_state.iter().enumerate() {
memory[(perm_base + idx as u64) as usize] = lane;
}
for r in 0..NUM_ROUNDS {
memory[(IP_BOUNDARY + perm_base + (r * ROUND_PERIOD) as u64) as usize] = KECCAK_RC[r];
}
}
let lane_cells: [Vec<Felt>; NUM_LANES] = array::from_fn(|lane| {
let base_perm = lane * perms_per_lane;
let lane_perms = num_perms.saturating_sub(base_perm).min(perms_per_lane);
let row_offset = base_perm * PERM_CYCLE;
let mut cells = Vec::with_capacity(height * LANE_WIDTH);
for r in 0..height {
let ip = IP_BOUNDARY + (row_offset + r) as u64;
let perm_in_lane = r / PERM_CYCLE;
let row_in_cycle = r % PERM_CYCLE;
if perm_in_lane >= lane_perms {
push_row(
&mut cells,
bpl_req,
ip,
&Slot {
op: Op::Nop,
back_a: 0,
back_b: 0,
dst_mult: 0,
},
0,
0,
false,
);
continue;
}
let spec = program[r % ROUND_PERIOD];
let act = row_in_cycle < active_rows_per_cycle;
let reads_a = !matches!(spec.op, Op::Nop);
let reads_b = matches!(spec.op, Op::Xor | Op::Andnot | Op::XorRol(_));
let a = if reads_a {
memory[ip.wrapping_sub(spec.back_a) as usize]
} else {
0
};
let b = if reads_b {
memory[ip.wrapping_sub(spec.back_b) as usize]
} else {
0
};
let r_val = simulate_logic(spec.op, a, b);
let c_val = simulate_rotate(spec.op, r_val);
if act && spec.dst_mult > 0 {
memory[ip as usize] = c_val;
}
push_row(&mut cells, bpl_req, ip, &spec, a, b, act);
}
cells
});
interleave_lanes(&lane_cells, height)
}