Compiler reasoning around a modulo counter

⚓ Rust    📅 2026-05-04    👤 surdeus    👁️ 2      

surdeus

I have a modulo counter -- basically this:

// It doesn't matter that first 0 is skipped
fn next_idx(ctx: &mut Ctx) -> usize {
  ctx.idx = (ctx.idx + 1) % MAX_IDX;
  ctx.idx
}

This works fine and it compiles down to what one expects; using a power-of-two MAX_IDX allows it to, on x86, reduce the core modulo counting down to an inc and an and.

But I stumbled on cycle() and played around with this:

struct Ctx {
  // initialized to (0..MAX_IDX).cycle()
  idxgen: iter::Cyclic<ops::Range<usize>>
}

fn next_idx(ctx: &mut Ctx) -> usize {
  ctx.idxgen.next().unwrap()
}

This compiles down to a fair bit more code. My naive hope, when I first saw cycle() was that the compiler would discover that it's a modulo counter, and deduce that the Option's None case (and thus its unwrap()) is dead code. I assume that the reason this isn't the case is that there's no compile-time information about the range (other than its type, of couse) -- meaning it can't assume that the start and end will not change during runtime.

Is there some way to constify the Range, such that the compiler could understand that it's an infinite iterator which will never yield None?

I took a few random stabs at it and the compiler very explicitly said that what I was trying to do wasn't allowed (and it didn't even say the usual "this is a nightly only feature"). What changes would be needed to allow the compiler to compile down the Cycle version down to the same compact inc/and code?

No XY apart from random curiosity.

1 post - 1 participant

Read full topic

🏷️ Rust_feed