Conditional compilation exclusive or

There are times where having exactly one attribute from a group is useful, for conditional compilation but having more than one should cause a compilation error. Consider the following code where we want a version of foo() for our code:


#[cfg(color_red)]
foo(_a: u32) -> Result<(), ()> { 
    Ok(()) 
};

#[cfg(color_blue)]
foo(_a: u32) -> Result<(), ()> {
    Err(())
};

#[cfg(color_green)]
foo(a: u32) -> Result<(), ()> {
    match(a) {
        0 => Ok(()),
        _ => Err(()),
    }
};

To provide a clear error right now is a complex combination of cfgs, that grows exponentially with the number of items in the group, e.g. for three color options we could use this:

#[cfg(not(any(color_red, color_blue, color_green)))]
compile_error!("At least one color must be set");
#[cfg(all(color_red, any(color_blue, color_green)))]
compile_error!("Only one color may be set");
#[cfg(all(color_blue, any(color_red, color_green)))]
compile_error!("Only one color may be set");
#[cfg(all(color_green, any(color_blue, color_red)))]
compile_error!("Only one color may be set");

I propose a new term, "one" which requires exactly one of the list of attributes, e.g.

#[cfg(not(one(color_red, color_green, color_blue)))]
compile_error!("Exactly one color must be set");

We are running into this issue when trying to select from multiple backends for a crate we are developing. I am thinking about trying to implement this as a patch to rustc and was interested in the community's thoughts.

1 Like

Nit: your error probably wants cfg(not(one(..))).

Good catch! I've edited to reflect this

Long-term I think there's general understanding that something needs to change with how features (or cfgs in general) are handled by Cargo, such that it's not necessary to handle this manually. But I also believe that's about the extent to which there is agreement — nothing concrete. As such having this as a stopgap seems reasonable, and quite simple to support I would imagine.

1 Like

To be precise, this should be “only” quadratic growth, not exponential.


Also, if you want a working version of this immediately, you can implement your own proc_macro that expands to this kind of code. E.g. a cfg_not_one macro such that

#[cfg_not_one(color_red, color_blue, color_green)]
STUFF…

would expand to

#[cfg(not(any(
all(not(color_red), color_blue, color_green),
all(color_red, not(color_blue), color_green),
all(color_red, color_blue, not(color_green)),
)))]
STUFF…
1 Like

It seems that this previously was rejected as RFC 2962:

5 Likes

Sounds like this previous internals thread: Cfg, keyword for "if multiple/more than one" , in which a stable solution to the "error at compile-time if not exactly one of these features is enabled" problem is provided.

At very least we need a way to specify in Cargo.toml that two features are mutually exclusive. There's tons of crates in the wild that need this feature and there are various tricks that are used to force compilation failure in case you enable two incompatible features.

Such features should only be enabled by the top-level crate (the binary crate that represents the final program), while library crates should generally refrain from enabling them for better compatibility (otherwise you could end up having two libraries being incompatible with each other because they both depend on a third library using incompatible features, causing the build to fail)

Looking at the previous proposals, perhaps what would be more appropriate (and useful) for rustc as opposed to Cargo, would a cfg evaluator to count the number of attributes set to true, you could then check if it's 1 (same functionality as exclusive or above) or any other value, e.g. if you need 2 or more features set.

note "exclusive or" is a misnomer if you want only one of a set of things, the exclusive or of a set of things is true if the number of things that are true is odd.

assert_eq!(false ^ false ^ false ^ false, false); // 0 things true -- even, so false
assert_eq!(false ^ false ^ false ^ true, true); // 1 thing true -- odd, so true
assert_eq!(false ^ false ^ true ^ true, false); // 2 things true -- even, so false
assert_eq!(false ^ true ^ true ^ true, true); // 3 things true -- odd, so true
assert_eq!(true ^ true ^ true ^ true, false); // 4 things true -- even, so false
1 Like

What you wrote is xor(xor(xor(a, b), c), d). This is consistent with one(one(one(a, b), c), d).

I would argue that "exclusive or" implies exactly one, rather than "odd", because 3 is very much not "exclusive". Imagine being exclusive with 3 girlfriends. The "odd" function is usually called parity(a, b, c, d). They just happen to be same for 2 arguments.

exclusive or with 3 or more inputs is basically always interpreted as being equivalent to just a ^ b ^ c ^ d ^ ... for inputs a, b, c, d, ...

In fact, out of my many years of experience in computer programming and digital electronics design, you are the first person that I recall choosing to not use the standard definition, so I would argue you're wrong or at least your use of "exclusive or" to mean "only one of" for more than two things is very misleading.

4 Likes

This could be called oneof or one, like in json-schema (in there, the equivalent of Rust's any is called anyOf, and the equivalent of Rust's all is called allOf, and they also have the operator oneOf that Rust doesn't have)

edit: oh, you proposed one already, yeah

Features should be additive, so does it really make sense to add support for a thing you shouldn't do?

2 Likes

Of course. After all, the keyword SHOULD, or the adjective "RECOMMENDED", means that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course.

In fact the section on additive features you have linked is directly followed by a section about mutually exclusive features, including recommending adding the very compilation error being requested in this thread (although it is simpler because there are only two features involved).

Note that to make this anything more than "should", one would need to open an issue on every crate that uses no_std instead of std as the feature selection to manage std:: usage.

Does anyone do that anymore? There was a very brief phase of it right at the start of people figuring out how to support conditional std usage, but it was very quickly realised that it doesn't actually work and everyone switched to having a std feature.

There actually are still some crates that use no_std where no_std actually is (sort of) additive, as the interface is no_std but the implementation for no_std requires pulling in more crates (e.g. array vectors, spinlocks, etc) which aren't used when std is available.

I still don't really agree with the use of no_std in this way, but it is actually fine with additive features.

That works for that crate in isolation, but it means dependents on it that need the traditional std feature will have to permanently enable the no_std feature and cause it to always use the less efficient method (or outright buggy method in the case of spinlocks on a non-preemptive system, and if you're claiming no-std support you shouldn't be assuming preemption).