use std::collections::HashMap;
use miden_air::lookup::{Challenges, LookupAir, ProverLookupBuilder, build_lookup_fractions};
use miden_core::{
Felt,
field::QuadFelt,
utils::{Matrix, RowMajorMatrix},
};
use miden_lifted_air::LiftedAir;
use rand::{Rng, RngExt, SeedableRng, rngs::StdRng};
use crate::{
math::{U256, from_limbs16, to_limbs16, to_limbs32},
primitives::byte_pair_lut::{BytePairLutAir, BytePairLutRequires, generate_trace as bpl_trace},
relations::{MAX_MESSAGE_WIDTH, NUM_BUS_IDS},
uint::{
CARRY_HI_BEGIN, CARRY_LO_BEGIN, COL_PTR, NUM_MAIN_COLS, PERIOD, TERM_CELL_GAP,
UintStoreAir,
mul::trace::UintMulRequires,
store_mul::{
NUM_MAIN_COLS as STORE_MUL_NUM_MAIN_COLS, UintStoreMulAir,
trace::generate_trace as store_mul_trace,
},
trace::{UintStoreRequires, generate_trace},
},
};
fn rand_qf(rng: &mut impl Rng) -> QuadFelt {
QuadFelt::new([Felt::from(rng.random::<u32>()), Felt::from(rng.random::<u32>())])
}
pub(crate) fn random_modulus(rng: &mut impl Rng) -> U256 {
let mut m: [u16; 16] = core::array::from_fn(|_| rng.random::<u16>().max(1));
m[15] = (rng.random::<u16>() & 0x7fff).max(0x100);
from_limbs16(&m)
}
pub(crate) fn random_uint_below(rng: &mut impl Rng, bound: U256) -> U256 {
let mut v: [u16; 16] = core::array::from_fn(|_| rng.random::<u16>());
v[15] = rng.random::<u16>() % to_limbs16(bound)[15];
from_limbs16(&v)
}
fn carries32(v32: &[u32; 8], comp32: &[u32; 8]) -> [u16; 7] {
let mut c = [0u16; 7];
let mut carry: u64 = 0;
for j in 0..7 {
let s = v32[j] as u64 + comp32[j] as u64 + carry;
carry = s >> 32;
c[j] = carry as u16;
}
c
}
fn sample_store(rng: &mut impl Rng) -> UintStoreRequires {
let bound = random_modulus(rng);
let mut store = UintStoreRequires::new();
let fp = store.pin_modulus(1, bound); store.intern_pinned(2, random_uint_below(rng, bound), fp); store.intern_pinned(3, U256::ZERO, fp); store
}
fn fold_balance<A>(
air: &A,
main: &RowMajorMatrix<Felt>,
challenges: &Challenges<QuadFelt>,
net: &mut HashMap<QuadFelt, Felt>,
) where
A: LiftedAir<Felt, QuadFelt> + Sync,
for<'a> A: LookupAir<ProverLookupBuilder<'a, Felt, QuadFelt>>,
{
let periodic = air.periodic_columns();
let combined = crate::tests::combined_lookup_main(air, main);
let lookup_main = combined.as_ref().unwrap_or(main);
let fractions = build_lookup_fractions(air, lookup_main, &periodic, challenges);
for &(multiplicity, denom) in fractions.fractions() {
*net.entry(denom).or_insert(Felt::ZERO) += multiplicity;
}
}
#[test]
fn uint_store_constraints_hold() {
let mut rng = StdRng::seed_from_u64(0xace1);
let store = sample_store(&mut rng);
let main = generate_trace(store, &mut BytePairLutRequires::new());
assert_eq!(main.height(), 16, "3 uints + 1 padding block × 4 rows");
let bound_row = 4 + 3;
let carried = (4..8).any(|j| main.values[bound_row * NUM_MAIN_COLS + j] != Felt::ZERO)
|| (12..15).any(|j| main.values[bound_row * NUM_MAIN_COLS + j] != Felt::ZERO);
assert!(carried, "random value block must carry (comp = bound − v borrowed)",);
crate::tests::check_local(UintStoreAir, &main);
}
#[test]
#[should_panic]
fn uint_store_rejects_pointer_zero() {
let mut rng = StdRng::seed_from_u64(0x0bad_0000);
let store = sample_store(&mut rng);
let mut main = generate_trace(store, &mut BytePairLutRequires::new());
for row in 0..PERIOD {
main.values[row * NUM_MAIN_COLS + COL_PTR] = Felt::ZERO;
}
main.values[(PERIOD - 1) * NUM_MAIN_COLS + TERM_CELL_GAP] = Felt::ONE;
crate::tests::check_local(UintStoreAir, &main);
}
#[test]
#[should_panic]
fn production_uint_store_rejects_pointer_zero() {
let mut rng = StdRng::seed_from_u64(0x0bad_c0de);
let store = sample_store(&mut rng);
let mut main = store_mul_trace(store, UintMulRequires::new(), &mut BytePairLutRequires::new());
for row in 0..PERIOD {
main.values[row * STORE_MUL_NUM_MAIN_COLS + COL_PTR] = Felt::ZERO;
}
main.values[(PERIOD - 1) * STORE_MUL_NUM_MAIN_COLS + TERM_CELL_GAP] = Felt::ONE;
crate::tests::check_local(UintStoreMulAir, &main);
}
#[test]
fn uint_store_buses_balance_against_bpl() {
let mut rng = StdRng::seed_from_u64(0xba1a);
let store = sample_store(&mut rng);
let mut bpl = BytePairLutRequires::new();
let uint_main = generate_trace(store, &mut bpl);
let bpl_main = bpl_trace(bpl);
let [alpha, beta] = [rand_qf(&mut rng), rand_qf(&mut rng)];
let challenges = Challenges::new(alpha, beta, MAX_MESSAGE_WIDTH, NUM_BUS_IDS);
let mut net: HashMap<QuadFelt, Felt> = HashMap::new();
fold_balance(&UintStoreAir, &uint_main, &challenges, &mut net);
fold_balance(&BytePairLutAir, &bpl_main, &challenges, &mut net);
let residual = net.values().filter(|m| **m != Felt::ZERO).count();
assert_eq!(
residual, 0,
"UintVal self-balances within the store; Range16 balances against BPL",
);
}
#[test]
#[should_panic]
fn uint_store_rejects_tampered_value() {
let mut rng = StdRng::seed_from_u64(0xbad5eed);
let bound = random_modulus(&mut rng);
let mut store = UintStoreRequires::new();
store.pin_modulus(1, bound); let mut main = generate_trace(store, &mut BytePairLutRequires::new());
main.values[0] += Felt::from(1u32);
crate::tests::check_local(UintStoreAir, &main);
}
#[test]
#[should_panic]
fn uint_store_rejects_out_of_range_value() {
let mut rng = StdRng::seed_from_u64(0xb00d_0035);
let bound = random_modulus(&mut rng);
let mut store = UintStoreRequires::new();
let fp = store.pin_modulus(1, bound); store.intern_pinned(2, random_uint_below(&mut rng, bound), fp); let mut main = generate_trace(store, &mut BytePairLutRequires::new());
let mut v = to_limbs16(random_uint_below(&mut rng, bound));
v[15] = to_limbs16(bound)[15] | 0x8000;
let v256 = from_limbs16(&v);
let comp256 = bound.wrapping_sub(v256);
let comp = to_limbs16(comp256);
let c = carries32(&to_limbs32(v256), &to_limbs32(comp256));
let base = 4;
for i in 0..8 {
main.values[base * NUM_MAIN_COLS + i] = Felt::from(v[i]);
main.values[(base + 1) * NUM_MAIN_COLS + i] = Felt::from(v[8 + i]);
main.values[(base + 2) * NUM_MAIN_COLS + i] = Felt::from(comp[i]);
main.values[(base + 2) * NUM_MAIN_COLS + 8 + i] = Felt::from(comp[8 + i]);
}
for (j, &cj) in c.iter().enumerate() {
let cell = if j < 4 {
CARRY_LO_BEGIN + j
} else {
CARRY_HI_BEGIN + (j - 4)
};
main.values[(base + 3) * NUM_MAIN_COLS + cell] = Felt::from(cj);
}
crate::tests::check_local(UintStoreAir, &main);
}
#[test]
fn uint_store_gaps_and_self_ref_padding() {
let mut rng = StdRng::seed_from_u64(0x6a9);
let bound = random_modulus(&mut rng);
let mut store = UintStoreRequires::new();
let fp = store.pin_modulus(1, bound);
store.intern_pinned(5, random_uint_below(&mut rng, bound), fp);
store.pin_modulus(100, U256::ZERO);
let mut bpl = BytePairLutRequires::new();
let uint_main = generate_trace(store, &mut bpl);
let bpl_main = bpl_trace(bpl);
crate::tests::check_local(UintStoreAir, &uint_main);
let [alpha, beta] = [rand_qf(&mut rng), rand_qf(&mut rng)];
let challenges = Challenges::new(alpha, beta, MAX_MESSAGE_WIDTH, NUM_BUS_IDS);
let mut net: HashMap<QuadFelt, Felt> = HashMap::new();
fold_balance(&UintStoreAir, &uint_main, &challenges, &mut net);
fold_balance(&BytePairLutAir, &bpl_main, &challenges, &mut net);
let residual = net.values().filter(|m| **m != Felt::ZERO).count();
assert_eq!(residual, 0, "non-trivial gaps + self-ref padding still balance");
}
#[test]
fn uint_store_empty_pads_to_one_block() {
let mut rng = StdRng::seed_from_u64(0xe39);
let store = UintStoreRequires::new();
let mut bpl = BytePairLutRequires::new();
let main = generate_trace(store, &mut bpl);
assert_eq!(main.height(), 4, "one padding block");
crate::tests::check_local(UintStoreAir, &main);
let bpl_main = bpl_trace(bpl);
let [alpha, beta] = [rand_qf(&mut rng), rand_qf(&mut rng)];
let challenges = Challenges::new(alpha, beta, MAX_MESSAGE_WIDTH, NUM_BUS_IDS);
let mut net: HashMap<QuadFelt, Felt> = HashMap::new();
fold_balance(&UintStoreAir, &main, &challenges, &mut net);
fold_balance(&BytePairLutAir, &bpl_main, &challenges, &mut net);
let residual = net.values().filter(|m| **m != Felt::ZERO).count();
assert_eq!(residual, 0, "an empty store still closes its buses");
}
#[test]
fn log_quotient_degree_matches_design_target() {
assert_eq!(crate::tests::log_quotient_degree(&UintStoreAir), 1);
}
#[test]
fn comp_hi_range_checks_are_load_bearing_at_its_new_position() {
let mut rng = StdRng::seed_from_u64(0xc0_ffee);
let store = sample_store(&mut rng);
let mut bpl = BytePairLutRequires::new();
let mut main = generate_trace(store, &mut bpl);
let comp_row = 4 + 2;
assert!(
main.values[comp_row * NUM_MAIN_COLS + 9] >= Felt::ONE,
"fixture needs a borrowable comp_hi[1] (reseed if not)",
);
main.values[comp_row * NUM_MAIN_COLS + 8] += Felt::from(1u32 << 16);
main.values[comp_row * NUM_MAIN_COLS + 9] -= Felt::ONE;
crate::tests::check_local(UintStoreAir, &main);
let bpl_main = bpl_trace(bpl);
let [alpha, beta] = [rand_qf(&mut rng), rand_qf(&mut rng)];
let challenges = Challenges::new(alpha, beta, MAX_MESSAGE_WIDTH, NUM_BUS_IDS);
let mut net: HashMap<QuadFelt, Felt> = HashMap::new();
fold_balance(&UintStoreAir, &main, &challenges, &mut net);
fold_balance(&BytePairLutAir, &bpl_main, &challenges, &mut net);
let residual = net.values().filter(|m| **m != Felt::ZERO).count();
assert_ne!(
residual, 0,
"an oversized comp_hi limb must unbalance Range16 — the checks are load-bearing",
);
}