Are impure `const fn`s possible?

For example, is it possible to write a const fn that returns a different number on every call?

const fn inc() -> usize {
   // magic
}

#[test]
fn test_inc() {
   assert_eq!(inc(), 0);
   assert_eq!(inc(), 1);
   assert_eq!(inc(), 2);
)

I've tried quite a few things to get this working, including:

  • Just a const usize
    • const values are immutable
  • static mut usize
    • can't convert static -> const
  • const UnsafeCell<usize>
    • const values are always passed by value, meaning that this solution will actually compile but the function returns 0 every time.
  • Creating a pointer:
    • const NUM: *mut u8 = std::intrinsics::const_allocate(...)
      • untyped pointers are not allowed in constants
      • Also, heap allocations seem generally very unstable anyway
    • const NUM: *mut usize = UnsafeCell::new(0).get()
      • This one comes surprisingly close to compiling, but encountered dangling pointer in final constant
    • libc
      • malloc is not a const fn

I'm at the point where I feel like I've almost tried everything so I was wondering if anyone on here had any ideas?

Thanks

I don't think this will work. A const function must be pure and every call must return the same result when it is given the same inputs.

From the Rust Reference:

Const functions have various restrictions to make sure that they can be evaluated at compile-time. It is, for example, not possible to write a random number generator as a const function. Calling a const function at compile-time will always yield the same result as calling it at runtime, even when called multiple times. There's one exception to this rule: if you are doing complex floating point operations in extreme situations, then you might get (very slightly) different results. It is advisable to not make array lengths and enum discriminants depend on floating point computations.

(emphasis, mine)

What sort of use case do you have in mind? Your example could be implemented without needing const functions, so I'm guessing there is some other scenario you had to simplify.

1 Like

Not currently, and fundamentally not during constant evaluation. It's an important quality that if you say ConstGeneric<{inc()}> in one place and I say ConstGeneric<{inc()}> somewhere else, we must get the same result for the type system to be sound.

However, you mustn't rely on const fn implying some form of purity. It's reasonably likely that the behavior of const fn will be allowed to diverge between compile time evaluation and runtime evaluation, including the potential for runtime evaluation to be properly impure. It's already the case that some unstable const fn have runtime behavior which refines the compile time behavior, e.g. guaranteed_eq and align_offset.

But it also depends on your definition of purity. It's currently disallowed to have cost &mut or const refs to cells, but these are both things which we'd like to support in the future.

4 Likes

I think it can be done with help of procedural macros.

// Xorshift128+
struct RNG {state: (u64, u64)}
impl RNG {
    const fn from_seed(seed: u64) -> Self {
        Self {state: (
            seed ^ 0xf4dbdf2183dcefb7, // crc32(b"0") concat crc32(b"1")
            seed ^ 0x1ad5be0d6dd28e9b  // crc32(b"2") concat crc32(b"3")
        )}
    }
    const fn rand_u64(mut self) -> (u64, Self) {
        let (mut x, y) = self.state;
        self.state.0 = y;
        x ^= x << 23;
        self.state.1 = x ^ y ^ (x >> 17) ^ (y >> 26);
        (self.state.1.wrapping_add(y), self)
    }
    const fn rand_u32(self) -> (u32, Self) {
        let (value, rng) = self.rand_u64();
        ((value >> 32) as u32, rng)
    }
}

const fn random_array<const N: usize>(seed: u64) -> [u32; N] {
    let mut rng = RNG::from_seed(seed);
    let mut ra = [0; N];
    let mut i = 0;
    while i < N {
        (ra[i], rng) = rng.rand_u32();
        i += 1;
    }
    ra
}

fn main() {
    const RA: [u32; 4] = random_array::<4>(0);
    println!("{:08x?}", RA);
}

Hm, that's unfortunate. Actually my use case is exactly this, generating incrementing numbers at compile time. I'm working on an alternative to ghost-cell that instead uses const generics, and it would be much better for the ergonomics of the crate if I could generate const usizes at compile time like this:

let t1 = gen_token(); //Token::<0>
let t2 = gen_token(); //token::<1>

I'd even be happy to do this in the form of a proc-macro but, as far as I'm aware, there's no way for proc-macros to know what "position" they've been called in.

You could parse the debug output of a span, which includes the line and column of the token. Doing so is (obviously) not guaranteed behavior, and can change at any time for any reason. I don't particularly advise you do this, but if it's absolutely necessary…

So with

let (t1, t2, t3, t4);
let mut first = true;
for _ in 0..2 {
    let a1 = gen_token!();
    let a2 = gen_token!();
    if first {
        t1 = a1;
        t2 = a2;
    } else {
        t3 = a1;
        t4 = a2;
    }
    first = false;
}
dbg!(t1, t2, t3, t4);

do you want 0, 1, 2, 3 or 0, 1, 0, 1? The former is strictly impossible (some code always evaluates to the same type); the latter is at least probabilistically possible via procedural macro powered const-random techniques (though it's still only probabilistic and duplicates are impossible to prevent because of separate compilation).

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.