What is an alternative for conditional evaluation like if or match in a Rust const function?

Viewed 1242

I have code which reads a config.toml file based on the environment name and provides all the config settings to the entire project.

const fn get_conf() -> toml::Value {
    let file_name = match env::var("ENVIRONMENT") {
        Ok(val) => val.to_lowercase(),
        Err(_e) => "development".to_lowercase(),
    };

    let content = fs::read_to_string(format!("conf/{}.toml", file_name)).unwrap();
    let conf: Value = toml::from_str(content.as_str()).unwrap();

    conf
}

static mut CONFIG: toml::Value = get_conf();

I get an error:

error[E0658]: `match` is not allowed in a `const fn`
 --> src/lib.rs:2:21
  |
2 |       let file_name = match env::var("ENVIRONMENT") {
  |  _____________________^
3 | |         Ok(val) => val.to_lowercase(),
4 | |         Err(_e) => "development".to_lowercase(),
5 | |     };
  | |_____^
  |
  = note: see issue #49146 <https://github.com/rust-lang/rust/issues/49146> for more information

This is solved in Rust nightly, but I don't want to use a nightly build for production. Is there any workaround for using a match or if condition in a const function?

3 Answers

From Rust 1.46 some core language features are now allowed in const fn.

const fn improvements

There are several core language features you can now use in a const fn:

if, if let, and match
while, while let, and loop
the && and || operators

The primary alternative for conditionals in const expressions is to cast booleans into usizes and use them as an index into an array of resulting values:

const fn demo(is_enabled: bool) -> i32 {
    let choices = [
        0,  // false
        42, // true
    ];
    
    choices[is_enabled as usize]
}

This can be (painfully) expanded to more and more conditions:

const fn sign(count: i32) -> i32 {
    let is_pos = count > 0;
    let is_zero = count == 0;
    
    [[-1, 0], [1, 0]][is_pos as usize][is_zero as usize]    
}

fn main() {
    dbg!(sign(-2));
    dbg!(sign(-0));
    dbg!(sign(2));
}

See also:


For your actual use case, you should just use a lazy global value:

use once_cell::sync::Lazy; // 1.4.0
use std::{env, fs};

static mut CONFIG: Lazy<toml::Value> = Lazy::new(|| {
    let file_name = match env::var("ENVIRONMENT") {
        Ok(val) => val.to_lowercase(),
        Err(_e) => "development".to_lowercase(),
    };

    let content = fs::read_to_string(format!("conf/{}.toml", file_name)).unwrap();
    toml::from_str(content.as_str()).unwrap()
});

See also:

It is not possible to use a const-fn with branching on stable yet indeed. Moreover, you are using env::var which is not const-fn, thus cannot be used in other const-fn. I believe you meant std::env! instead, which retrieves an environment variable during compile-time, rather than execution.

Fow now one possible "workaround" is a build script, it's a little bit clumsy yet powerful. With this approach flow would be as following:

// build.rs
use std::path::Path;
use std::env;
use std::fs::File;

fn main() {
    let path = Path::new(env::var("OUT_DIR").unwrap().as_str()).join("gen.rs");

    File::create(path)
        .expect("gen.rs create failed")
        .write_all("<your generated content here>")
        .expect("gen file write failed")
}

Then you can use generated file with a direct file-include:

// main.rs or any other file

include!(concat!(env!("OUT_DIR"), "/gen.rs"));
Related