3

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?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Amber More
  • 131
  • 1
  • 15
  • 3
    Hi! I can't get it running on nightly either: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=ab91a243c239b68c6f4db193ec423816. Moreover, I have doubts if reading environment variables in a `const fn` can be done resp. gives meaningful results. Are you sure you want a `const fn` or are you merely looking for something to initialize `CONFIG`? If so, `lazy_static` may help you. – phimuemue Jun 23 '20 at 07:54
  • 2
    Const functions get executed *at compilation time* so if it worked this would completely freeze your configuration as what it was when the software was *compiled*, which doesn't seem like the normal use for a configuration file. As @phimuemue said, are you sure it's not `lazy_static` you want, such that the configuration file is read once when the software is initialised? – Masklinn Jun 23 '20 at 08:10
  • lazy_static seems to be a great solution but when I ran lazy_static! {pub static CONFIG :Value = get_conf();}, it gives me errors "no rules expected this token in macro call" and "`match` is not allowed in a `const fn`". – Amber More Jun 23 '20 at 08:29

3 Answers3

4

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
zerocewl
  • 11,401
  • 6
  • 27
  • 53
2

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:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Code compiles well, but while debugging with CLion getting CONFIG value "-var-create: unable to create variable object". – Amber More Jun 24 '20 at 06:16
1

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"));
Kitsu
  • 3,166
  • 14
  • 28
  • `env!` will fail to compile if the envvar is absent though, so either there's no failure mode to handle or it needs to use `option_env!` (but then there's the same issue that const fns can't currently do conditions). – Masklinn Jun 23 '20 at 08:08