6

I'm trying to calculate the maximum value of a set of constants at compile time inside a Rust procedural macro (a derive macro).

The macro looks something like:

fn get_max_len() -> TokenStream {
    // Each TokenStream represents a constant expression
    let len: Vec<TokenStream> = get_constant_lengths();

    quote! {
      // #(#len),* gets expanded to #len[0], #len[1], #len[2]...
      const LEN: usize = std::cmp::max(#(#len),*);
    }
}

The problem is that std::cmp::max is a function and hence can't be used inside a constant expression (at least until const fn is stabilized - I want to keep to stable Rust if at all possible).

How can I calculate the max of a set of constants at compile-time?

I might be able to write a max! macro that basically constructs a huge chain of ifs recursively, but I'm hoping there is a cleaner solution out there.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Richard Matheson
  • 1,125
  • 10
  • 24
  • 1
    Why not just evaluate `max` *outside* of the `quote` macro call and only put the resulting value inside? – Shepmaster Dec 04 '18 at 19:08
  • Unfortunately the constants in that case are commonly associated constants from interfaces, so I don't have direct access to the value - what I have are expressions that will be evaluated by the compiler as constants. In order to work out the values, I'd have to implement an evaluator that can extract the value, recursively in some cases, which is infeasible. – Richard Matheson Dec 04 '18 at 19:14
  • 1
    *a huge chain of `if`s* — `if` is not currently allowed in constant expressions either; is that what you meant? – Shepmaster Dec 04 '18 at 19:14
  • yes that was what I meant - if that's not allowed I'm completely out of ideas... – Richard Matheson Dec 04 '18 at 19:15
  • 2
    You can use `[a, b][(a < b) as usize]` to compute the maximum of two values at compile-time. I'm gonna leave it to the reader to work from that snippet to a general solution for more elements. – oli_obk Dec 04 '18 at 19:20
  • @oli_obk that might just work - a very neat trick - will try it and see. Thank you! – Richard Matheson Dec 04 '18 at 19:22
  • @oli_obk that works perfectly. If you write it as an answer I'll mark as accepted. – Richard Matheson Dec 05 '18 at 13:25
  • For reference, I ended up using`len.iter().fold(quote!(0), |max_val, expr| { quote!([#max_val, #expr][(#max_val < #expr) as usize]) })` The generated rust is pretty horrific, but it compiles away to a single beautiful constant :) – Richard Matheson Dec 05 '18 at 13:32

1 Answers1

12

While constant evaluation does not support if or other control flow, there is a way to select values depending on binary conditions:

[a, b][(a < b) as usize]

What this does is

  • create an array of the two elements you want to choose between
  • create an arbitrary boolean expression
  • cast said expresison to a usize
  • use that value to index into the array created above

The first element is chosen if the condition is false, the second element is chosen if the condition is true.

While this scheme can theoretically be extended to arbitrary length arrays by computing indices via mathematical operations on multiple casted bools, it seems simpler to just go the functional way and nest the above expression:

const fn max(a: usize, b: usize) -> usize {
    [a, b][(a < b) as usize]
}

const MAX: usize = max(max(max(5, 6), 42), 3);

Starting with Rust 1.31, const fn is usable on the stable compiler.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
oli_obk
  • 28,729
  • 6
  • 82
  • 98