[Pre-RFC] Cfg Attribute Alias

I had this idea a few weeks ago and then found this existing issue filed in the RFC repository that was similar to what I was thinking. This might be small enough to not need a Pre-RFC but given that it would be my first RFC I decided to first gather some feedback in here to see if other people would find this useful or not, and to check if I got anything grossly incorrect. Thanks in advance for any feedback I get on either the writing style, matter of explaining, and of course the feature being introduced.

Summary

Allows adding an alias for an existing #[cfg(...)] attribute or combination of attributes.

The proposed syntax is:

#[cfg(my_alias)] = #[cfg(all(complicated, stuff, not(easy, to, remember)))]

Motivation

It is not uncommon for #[cfg(...)] conditional compilation attributes, referred to as cfg attributes going forward, to be composed of other properties. For example, the following cfg attribute prevents code from being compiled on target architectures that are not x86 or x86_64:

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]

Because there is currently no concise way to refer to this cfg attribute, it is repeated verbatim 481 times in multiple files across the rust compiler and standard library.

This RFC proposes introducing aliases for cfg attributes, allowing developers to encapsulate a long cfg attribute into single reusable name.

If the above changes are accepted, developers could write the following:

#[cfg(any_x86)] = #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]

#[cfg(any_x86)]
fn function_compiles_only_on_x86_target_archs() {
  //...
}

#[cfg(not(any_x86))]
fn function_compiles_only_on_not_x86_target_archs() {
  //...
}

The new #[cfg(any_x86)] attribute could be used anywhere that #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] is currently used and have the same effect.

Another concrete example comes from the popular crate serde, in which the author found 772 uses of #[cfg(any(feature = "std", feature = "alloc"))]. This RFC would allow serde developers to write an alias that gives meaning to the combination of these features and use that single name throughout their codebase.

The above examples contain only two cfg attributes wrapped in an any. The feature introduced in this RFC will also help with more complex examples such as:

#[cfg(test, not(any(feature = "integration", feature = "contract")))] //run tests when both the integration and then contract feature flags are off
mod test() {
  //... run your fast tests
}

#[cfg(all(test, feature = "integration"))] //run tests when the integration flag is on
mod test() {
  //...do some slower tests with the real services mocked
}

#[cfg(all(test, feature = "contract"))] //run tests when the contract flag is on
mod test() {
  //...run some contract tests to verify responses of real network calls
}

The intention of the above snippet is to be able to control which test gets run depending on a feature flag, and it is succesful at it. However, the author argues that the above combination of cfg attributes is confusing an unintuitive as to what the intention of them are without leaving comments every time the specific combination of cfg attributes is used.

This feature will allow the user to name these concepts through an alias to help readibility and re-use of the logic:

#[cfg(integration_test)] = #[cfg(all(test, feature = "integration"))]
#[cfg(contract_test)] = #[cfg(all(test, feature = "contract"))]
#[cfg(unit_test)] = #[cfg(all(test, not(any(integration_test, contract_test))))]

After these aliases have been introduced, the user can use these named cfg attributes to conditionally compile (and therefore run) different tests:

#[cfg(unit_test)]
mod test() {
  //... run your fast tests
}

#[cfg(integration_test)]
mod test() {
  //...do some slower tests with the real services mocked
}

#[cfg(contract_test)]
mod test() {
  //...run some contract tests to verify responses of real network calls
}

The proposed syntax for cfg aliases is meant to reflect the current way in which Rust allows for aliasing of other types or traits.

// alias types:
struct MyComplicatedStruct;
type MyStruct = MyComplicatedStruct;

// alias enums per RFC 2338:
enum MyComplicatedEnum {};
type MyEnum = MyComplicatedEnum;

// alias traits per RFC 1733:
trait MyComplicatedTrait;
trait MyTrait = MyComplicatedTrait;

// alias conditional compilation attributes per this RFC:
#[cfg(my_attr)] = #[cfg(my_complicated_attribute)]

[1] numbers based on rustc’s 2018-04-01 nightly build using command: ag "^\s*#\[cfg\(((.|\n)*?)\]" -o | sed 's/.*://' | sed 's/^[ ]*//' | sed '/^$/d' | sed ':a;N;$!ba;s/,\n/,\t/g' | tr -d " \t" | sort | uniq -c | sort -nr

[2] numbers based on serde’s v1.0.37 release using command: ag "^\s*#\[cfg\(((.|\n)*?)\]" -o | sed 's/.*://' | sed 's/^[ ]*//' | sed '/^$/d' | sed ':a;N;$!ba;s/,\n/,\t/g' | tr -d " \t" | sort | uniq -c | sort -nr

Guide-level explanation

A #[cfg(...)] attribute, or cfg attribute, is used to control the compilation, and therefore execution, of certain code routes. Since a single attribute is often insufficient to fully express the range of control needed, Rust allows you to combine cfg attributes through the following syntax:

#[cfg(any(x, y, ...))]

#[cfg(all(x, y, ...))]

#[cfg(not(x))]

However, repeating combinations of attributes, especially when they become lengthy, can be frustrating and a cause of errors. This is why Rust allows you to provide aliases for cfg attributes.

//main.rs or lib.rs
#[cfg(my_alias)] = #[cfg(all(some, long, complicated, not(easy, to, remember)))]

An alias name follows the same rules as identifiers in Rust.

#[cfg(my_alias3)] = #[cfg(foo)]   // good
#[cfg(myAlias)] = #[cfg(foo)]     // good
#[cfg(3myAlias)] = #[cfg(foo)]    // error, cannot start with a number
#[cfg(my alias)] = #[cfg(foo)]    // error, cannot contain spaces
#[cfg(alias=false)] = #[cfg(foo)] // error, contains non-allowed special character (`=`)
#[cfg(_)] = #[cfg(foo)]           // error, cannot be only underscore

Aliases must be introduced at the root module of a crate and can be used anywhere in the crate.

//START OF LIB.RS

mod inner;

#[cfg(my_alias)] = #[cfg(all(some, long, complicated, not(easy, to, remember)))]

#[cfg(my_alias)]
fn some_fn() {
  //...
}

//END OF LIB.RS

//START OF INNER.RS

#[cfg(my_alias)] //this will work because the alias was introduced in lib.rs
fn conditional_fn() {
  //...
}

//END OF INNER.RS

cfg aliases are not exposed between crates. If in your binary you are using the common pattern of having a internal lib crate that gets exported to your main.rs, then any cfg alias declared in your main.rs will NOT be exposed to the files in your lib crate. Similarly, any cfg alias declared in your lib crate will not be exposed to your main.rs binary.

Trying to declare a cfg alias outside of your root module (lib.rs or main.rs) will result in a compilation error.

//lib.rs

#[cfg(my_alias)] = #[cfg(any(foo, bar))] // this will compile

mod inner {
  #[cfg(my_other_alias)] = #[cfg(any(foo, bar))] // this will *not* compile
}

Reusing an alias name will result in a compile time error:

#[cfg(foobar)] = #[cfg(any(foo, bar))]
#[cfg(foobar)] = #[cfg(any(x))] // error. foobar already exists

As will giving a cfg alias the same name as a preexisting cfg attribute:

//compiled using `--cfg=foobar` flag in rustc will add `foobar` as a cfg attribute.

$[cfg(foobar)] = #[cfg(any(foo, bar))] // error. foobar already exists

Reference-level explanation

Any cfg alias should behave exactly as its expanded, non-aliased form and be valid wherever any cfg attribute is valid.

An alias name follows the same rules as identifiers in Rust.

Aliases will only be allowed at the top of a crate, main.rs for a binary, or lib.rs for a library. They will be useable anywhere within the crate.

Aliases are not exportable between crates, and a binary that is split into one or multiple library crates will not be able to use the aliases declared in its internal library crates.

Aliases will produce an error at compile- time if an attribute with the same name already exists, either due to an existing alias, or an existing configuration attribute, or passed through the cfg=... flag in rustc.

Drawbacks

This RFC introduces another way to construct cfg attributes, potentially making it harder to find why a path of code was compiled or not.

It is the belief of the author that by blocking the export of cfg aliases, it becomes the responsibility of the developer to be aware of what cfg attributes have been aliased within their crate.

With this design, there is no immediate way to differentiate between an aliased cfg versus one introduced through a compiler or feature flag.

It is belief of the author that as a user of this feature, I should not need to know the source of a cfg attribute (i.e., alias, intrinsic, or compiler flag). If necessary, a text search within my root module would show me whether the cfg is an alias, and if so, what it aliases.

With this design, there is potential of introducing a breaking change if a new intrinsic attribute is introduced whose name matches an existing alias on a crate. The author is unsure about the real life potential of this breaking change. In practice newer attributes could be introduced like this: #[cfg(new_attribute = value)] which is explicitely not allowed as an alias since it contains spaces and a special character and thus no breaking changes would be introduced when adding new cfg attributes.

Rationale and alternatives

Syntax Alternatives

An existing issue proposed the following syntax:

#![cfg_shortcut(foo, any(super, long, clause))]

for introducing “shortcuts” of existing cfg attributes.

This alternative syntax provides the same functionality as the syntax proposed in this RFC, but it introduces the concept of a “shortcut”, which would be a new term in the language.

The author believes that referring to this concept as an alias rather than a shortcut is a better match for the current terms used in Rust. The syntax proposed in this RFC also more closely matches the syntax currently used for aliasing types and traits.

Alternatively, aliased cfg attributes could be identified by a special prefix. For example, an alternative design could force aliases to be prefixed with $:

#[cfg($foobar)] = #[cfg(any(foo,bar))]

#[cfg($foobar)]
fn conditional_function() {
  //...
}

This achieves the same goal as the originally proposed syntax, with the advantage of easier identification of whether a cfg attribute is an alias and making it harder for a new cfg attribute introduced by the compiler to conflict with an alias.

The disadvantage is that this design will now expose what the author believes to be an implementation detail. The fact that the cfg attribute comes from an alias should not be important at the moment of use.

This syntax also means that no cfgs will be allowed through a compiler flag that start with $.

The author could not find existing examples of Rust blocking prefixes on cfg attribute names.

Visibility alternatives

The proposed RFC blocks the introduction of cfg aliases anywhere but in the root of the crate (main.rs for binaries or lib.rs for libraries). An alternative would be to allow cfg aliases to be introduced anywhere and expose those aliases to any child of that module. This will allow for declaration of cfg aliases only where needed.

However, allowing cfg alias declarations anywhere increases the problem of not knowing where a cfg alias originated, since it could have come from any parent module instead of only at the root.

Implementing the RFC as-is does not prevent this extension from being added later and would not cause breaking change. The author recommends revisiting this point after the current implementation is stable and enough feedback is gathered on whether declaring cfg aliases would be useful in non-root modules.

Another alternative would be to allow the cfg alias in any module, but not expose it to any other module, child or parent. The author believes this would severely limit the use case of aliasing cfg attributes since the logic would have to be copied across modules within the same crate.

Prior art

There is an existing issue filed in the RFC repository that proposes a change very similar to this one, but with different syntax.

Currently, Rust allows introducing aliases of structs and enums, with an approved RFC for aliasing traits. This will extend the concept of aliasing to cfg attributes.

Unresolved questions

  • Does Rust currently have limitations on the names of cfg attributes?
3 Likes

To avoid introducign new syntax concepts this would be best worked within the attribute system:


#![cfg_alias(foo, any(bar, baz))]

#[cfg(foo)] pub mod bar;

and mandate that cfg_alias may only appear as a crate attribute, and only exists after the point of declaration.

8 Likes

This also works, and it is potentially easier to implement but my concern is ergonomics and similarity with other concepts in Rust.

Ergonomics-wise, I find

#[cfg(foo)] = #[cfg(any(bar, baz))]

easier to parse than:

#[cfg_alias(foo, any(bar, baz))]

particularly as the attribute being alias becomes more complex.

Additionally, I think it mirrors better with the way we alias types/traits in Rust:

type MyStructAlias = MyStruct;
type MyEnumAlias = MyEnum;
trait MyTraitAlias = MyTrait; //from a different RFC

If the current Rust syntax for aliasing types was alias type MyAlias = Foo then that would be different story and I would wholeheartedly agree.

That being said, I am open to this syntax if my views on what “reads clearer” is in the minority.

Thanks for reminding me of ![cfg(...)]. It completely skipped my mind that crate-wide attributes were already a thing.

A few notes:

The grammar allows for arbitrary token trees after the attribute head. No official attributes use this, and probably much code doesn’t fully handle arbitrary token trees, but the syntax could be #![cfg_alias foo = all(bar, baz)]. Another, less alien possibility: #![cfg_alias(foo = all(bar, baz))].

Using the attribute-assignment is a new syntax, as the attributes aren’t attached to anything. I’m more in favor of attaching the alias attribute to the crate via inner attributes. Then there’s an easy way to semantically restrict it to the root module as well.

Defining an alias in-attribute composes better.

#![cfg_alias(x86y = any(target_arch = "x86", target_arch = "x86_64"))]
#![cfg_alias(foobar = all(feature = "foo", feature = "bar"))]
#![cfg_alias(power = all(x86y, foobar))]

If you only use #[cfg(alias)] = #[cfg(...)], the composability doesn’t naturally fall out of the syntax, as you aren’t “assigning” to alias but to #[cfg(alias)].

Whatever solution is adopted, let it also work with #[cfg_attr], please. I want my #[cfg_attr_alias(Serde = (feature = "serde", derive(Serialize, Deserialize)))].

Is this “solved” by macros? Can I do the following?

macro #[dox] = #[cfg(feature = "dox")]

Should I be able to?

4 Likes

I fully agree with @Manishearth in that we should not unnecessarily change the syntax of attributes, and that these kind of aliases are better declared as crate attributes. Then you also know where to put and find them!

If the idea of cfg aliases is well-received then it can be extended to other kinds, e.g.:

#![derive_alias(Pod, derive(Copy, Clone, PartialEq, Hash, Debug))]

I hastily suggested general attribute aliases, like this #![alias(Pod, derive(..., ...)], but that would lead to the creation of custom top-level attribute identifiers. I don’t think we want that.

This is actually quite an interesting idea! This also raises questions about how / if you can export such an alias outside of the crate.

I love that derive alias idea. It’s definitely better than spamming #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] everywhere.

1 Like

Overgeneralization bikeshed:

#![alias(foobar, all(foo, bar))]
#![alias(clippy_allow($lint), cfg_attr(feature = "cargo-clippy", allow($lint)))]

#[cfg(foobar)]
#[clippy_allow(all)]
fn hi_mom() {}

This is likely too deep. I really want to be able to wrap up some common cfg_attrs, but that requires top-level attribute aliasing. Though, if #[macro] proc-macro happens, we get that anyway.

And the base idea that you expand these alias words anywhere breaks for custom attributes, as they can give meaning to arbitrary identifiers within their namespaces, and the compiler knows nothing about that.

Any aliasing solution unfortunately probably has to be by-top-level-attribute, as attribute’s internal syntax is allowed to vary.

1 Like

@CAD97

regarding your comment

Blockquote If you only use #[cfg(alias)] = #[cfg(...)], the composability doesn’t naturally fall out of the syntax, as you aren’t “assigning” to alias but to #[cfg(alias)].

It was my intention with the currently proposed syntax to allow for composition, which would look like this in your example:

#[cfg(x86y)] = #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#![cfg(foobar)] = #[cfg(all(feature = "foo", feature = "bar"))]
#![cfg(power) = #[cfg(all(x86y, foobar))]

That being said, I can see the value of using #![cfg_alias(foo = bar)], since it allows us to re-use crate attributes to restrict the use to the tip of the crate only and I think I will include it as the suggested syntax when I propose this as an RFC and include the currently proposed syntax as an alternative. Thanks!

@repax

The current proposal does limit the cfg alias to only be declared in the root module, that being said I think using #![cfg(...)] to do this is exactly what we should do and I am a little embarrassed I didn’t think of it.

I also thought about aliasing other attributes, but I wanted to start small which also made me lean towards the currently proposed syntax since it could be extended later:

#![cfg(foo)] = #[cfg(any(bar, baz))]
#![derive(Foo)] = #[derive(Bar, Baz)]

which I think mirrors a little better than:

#![cfg_alias(foo = any(bar, baz))]
#![derive_alias(Foo = Bar, Baz)]

I am unsure which other attribute could want to be aliased, but if there were more then I think the “similarity” of how they alias would be more valuable.

I also want to try to keep this RFC a little focused on specifically to aliasing cfg attributes, while leaving room for aliasing other attributes in the future without the syntax looking off.

@Centril For this RFC I explicitly decided to not allow exporting aliases outside of a crate to keep things simple. I am personally fine including the aliases that I care about in my crate manually rather than bringing in a crate simply to re-use their aliases. Although I am open to being wrong.

@CAD97 I like the idea of having a more general alias attribute! I think whichever solution we decide should allow for expansion of the alias idea to other attributes without two different syntaxes being at odds with each other. I am unsure if this RFC should be generalized to all attribute cases, but it is good to keep in mind. Thanks.

I think that makes sense as a start for #[cfg]; my comment was more about #[derive_alias(..)] where I think exporting is more important :slight_smile:

How about the following syntax as a compromise and allows for extensibility of future aliases?

#![alias(cfg(foobar) = cfg(any(foo, bar)))]

This allows in the future to extend the syntax for:

#![alias(derive(Data) = derive(PartialEq, Eq, Clone,Copy, PartialOrd, Ord))]
#![alias(cfg_attr(serde) = cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

I think I like this better than having cfg_alias and derive_alias and cfg_attr_alias, etc as it is just one “new” keyword to alias any attribute rather than a new keyword per attribute.

I don’t think these declarations are compatible with the current syntax. That might not be a big problem, idk.

The following should be ok, but is probably less clear/obvious:

#![alias(cfg(foobar), cfg(any(foo, bar)))]

There would need to be some restrictions in place to stop declarations such as:

#![alias(cfg(Foo), derive(PartialEq, Eq, Clone,Copy, PartialOrd, Ord))]

Here’s an idea for less parenthesis:

#![alias cfg(foobar) for cfg(any(foo, bar))]

(Inspired by impl Trait for Type)

It would be nice to stay within the current syntax, although I imagine it wouldn’t be too hard to do a non-breaking change to allow this.

Since we would probably restrict aliases to only alias specific attributes then maybe something like this could work:

#![alias(foobar = cfg(any(foo, bar)))]
#![alias(Data = derive(Eq, PartialEq, Clone, Copy, Debug))]
#![alias(Serde = cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

//usage
#[cfg(foobar)] // foobar is a cfg alias so it can only be used within a cfg
fn only_called_when_foo_or_bar_are_on() {
  //...
}

#[derive(Data)] // Data is a derive Alias so it can only be used within a derive
struct Foo {
  //...
}

#[cfg_attr(Serde)] // Serde is a cfg_attr alias so it can only be used within a cfg_attr
struct CanSerialize {
  //...
}

Alternatively we could also alias the entire attribute and do:

#![alias(foobar = cfg(any(foo, bar)))]

#[foobar] // foobar aliases cfg(any(foo, bar)) so no need to re-wrap it a #[cfg(...)]
fn only_called_when_foo_or_bar_are_on() {
  //...
}

This last option however feels weird to me because now foobar looks like a custom attribute rather than a simple alias.

1 Like

I think for to signal equality would be quite different than impl Trait for Type, but I do like that there are less wrapping parentheses going around. This would also be a fairly big departure from the currently allowed syntax for attributes and I would like to stay as within that as possible.

@nrxus I think you needn’t be too worried about similarity to the current syntax. #![alias(foobar = cfg(...))] already breaks on the current compiler. Can’t get any worse.

#![alias cfg(foobar) = cfg(...)]

would also look nice.

It should look familiar to what is already there, though

If we allowed macros in an attribute (we do have considered #[doc = include_str!("README.md")] before), then it could simply be

macro my_alias() {
    all(complicated, stuff, not(easy, to, remember))
}

#[cfg(my_alias!())]
struct Stuff;
2 Likes

#![cfg(my_alias!())] wouldn’t work, though.