Easy conditional compilation of experiments in code

After poring over the docs, it seems reasonably clear that there's no way to do something like:

#[set(method = "release")]

#[cfg(method = "release")]
fn do_something() {
// do it the old way
}

#[cfg(method = "experimental")]
fn do_something() {
// try a new way to do this
}

ie., conditional compilation based on something I set in the source file.

It took me a while since it seems that not being able to do that is an omission. I suppose yes, I can modify my build scripts to select this, based on an environment setting, maybe a helper in Cargo.toml, yadayada. But this is just for quick experimentation.

So, what's the rationale for it working this way?

(The alternative is:

fn xdo_something() {
// do it the old way
}

fn do_something() {
// do it the new way
}

swapping the x back and forth, and living with the compiler kvetching about unused functions.

)

You could make a macro? One easy option:

if_not experiment! {
fn foo(){}
}
if_experiment!{
fn foo() {}
}

which you just define as being empty or repeating the input tokens. You could fancy this up, if you find it useful.

1 Like

Thanks for the reply, I guess this would work. Not sure what it has over the 'xfoo' thing, though I haven't digested it yet.

I suppose I should clarify that I'm not really asking for a way to do this, I'm asking why the way I'd do it in C (grizzled old C programmer) isn't supported in Rust --- is there a philosophical reason such that it's considered harmful? I'm open to "here's why this is a really bad pattern" responses.

.. the old C way:

#define experimental

#ifdef experimental
void do_something() { /* new way / }
#else
void do_something() { /
old way */ }
#endif

1 Like

The biggest difference is that you can control all the locations at once, otherwise yeah it's not all that useful.

But it actually kind of is the equivalent! A mechanism to replace/expand the source token stream. There's lots of reasons why people have claimed the C preprocessor is pure evil (though I would hardly go that far :sweat_smile:,) the rust macro system is a more "principled" approach that mostly doesn't matter for this case, but in other situations fixes several issues with name conflicts you can hit, for example.

The first thing that comes to mind is modules. Not sure how convenient is this for your workflow.

mod experiment {
    pub fn foo() -> i32 {
        42
    }
}

mod release {
    pub fn foo() -> i32 {
        73
    }
}

// pub use release::*;
pub use experiment::*;

fn main() {
    println!("{}", foo());
}
1 Like

Rustaceans do this all the time using features.

# Cargo.toml

[features]
experimental = []
// src.rs

#[cfg(not(feature = "experimental"))]
fn do_something() {
    // do it the old way
}

#[cfg(feature = "experimental")]
fn do_something() {
    // try a new way to do this
}

Given you've already read the docs, perhaps you don't consider Cargo.toml as part of the source code… but you really should. Modules are more suitable when you might use both versions at the same time.

Agreed, I wasn't thinking of Cargo.toml as source code. I guess the idea was that a small experiment "should" be handled in as concentrated a part of the source code base as possible (so don't mess with 'features' if it's just a local optimization attempt).

I'm still interested in why the ability to do (something like)

#[set_cfg(foo, bar = "grill")
..
#[cfg(foo, not(bar="bake"))]
....

(specifically, the 'set_cfg') isn't supported. Is it a philosophical thing, where that sort of configuration (in C, interrelated ifdefs) taken too far, is considered to end badly (so, take away the "dangerous toys")? I don't see how putting that same potential complexity in Cargo.toml buys anything. (Ok, maybe what it does buy is "only one place to look"... and perhaps that's enough.)

I don't have a citation, but by making the things that drive conditional compilation static, global inputs to the compilation, one avoids many questions and complexity around scoping, ordering, shadowing, and hygiene. Not to mention implementation strategies like parallelism and incremental compilation.

2 Likes

It's more that the c preprocessor was considered a bad solution to the problem, for several long established reasons since now you can't even reliably parse any arbitrary source file. As for why there isn't specifically a set_cfg attribute? Probably no reason other than features don't exist until they're designed and implemented - we have lint configuration inline after all and that's morally the same - and the fact that you already have macros to locally implement near-arbitrary source transformations

Thank you for this, this seems to corroborate my thought that "this way lies madness". Doesn't solve my problem, but does seem to answer why that tool has been confiscated.

Thanks! I'm seeing a bit of "this is a way to chaos"... but your reply does suggest that "nobody has yet asked for this". Am I interpreting this correctly?

I wouldn't quite say nobody has asked for this; the ability to write cfg(mycfg) as an alias of some larger and more convoluted cfg is often asked for, and the initial suggestion is not uncommonly to point at how setting custom config flags would make such trivial. But it really is a case where arbitrarily setting config creates issues that keeping it global avoids.

And if your signature isn't changing, it's sufficient to have an if EXPERIMENTING branch inside the fn, no need to separately define the full item.

Also: an RFC for #[cfg(false)] was recently accepted, so in the future you'll be able to slap that on the inactive item instead of renaming. Or use #[cfg(any())] today for the same effect.

Thanks for all the replies. In the event, I decided to take a different route (command-line parameter) instead, to allow quicker comparisons.