Rust 2030 Christmas list: Better cfg

Discussion on r/rust

12 Likes

What a neat concept. I really like the idea of the compiler being able to do some level of validity checking on items that are #[cfg]-ed off in the current compilation.

To move forward with the idea, I think we'd need some sense of how many existing crates would be broken by the new constraints. I'm particularly worried about fields and variants that may or may not exist depending on the configuration.

1 Like

I think I remember some discussion about having to do this for reasonable IDE support as well. E.g. imagine having to rename a function which may not even be typoed but where the usage is cfg'd off. I don't know where all the tools are on this. I love this idea (even though I'm basically mono platform).

3 Likes

I feel the pain, but the trade-offs for this are hard to swallow. For example, I wouldn't want to lose optional struct fields.

There is an RFC for portability lints:

1 Like

In theory, if foobar has N features, to be sure that possible configuration is error-free, you would need to run the CI 2^N times for every supported platform

I think if it was guaranteed that features are additive, it would be enough to run the CI 2 times for every supported platform: Once with all features disabled, once with all features enabled.

This begs two questions: First: What should crates like druid do that require mutually exclusive features? And second: Can the compiler enforce that features are additive?

To allow mutually exclusive features, there have been multiple RFCs already, so it seems like people are interested in this. My currently favourite solution is what I call "custom cfg keys". For example, druid could use a backend key:

#[cfg(backend = "gtk")]

In the Cargo.toml, it could be defined like this:

[cfg.backend]
gtk = ["gtk-sys"]
x11 = []

Enforcing that features are additive seems pretty difficult to me. But even if it can't be enforced, having better support for mutually exclusive features would be a net win I believe.

It wouldn't solve the main problem discussed in the post (If it compiles on my machine, it should compile everywhere), but it is easier to achieve (it is a smaller change to the compiler than the proposed solution in the post, and is backwards compatible). Furthermore, the solution outlined by @PoignardAzur doesn't really solve the problems of mutually exclusive features, so it's orthogonal to "custom cfg keys".

If you think that this is off-topic, feel free to split it into a new discussion.

1 Like

That doesn't cover programmer errors like referencing stuff that is defined in one feature from another feature. A common example would be accidentally using a std:: type from within #[cfg(feature = "alloc")] code. If there are no interactions between features, then N jobs would be enough to test them each individually, but unfortunately features are more correctly multiplicative than additive, you can have code that is only active when multiple features are active, and you could have the same sort of errors with referencing stuff from a different feature in these sorts of intersections that need testing for.

Pretty much the only times I've seen the need for this is

There are more situations. For example, rapier could use mutually exclusive features to select the floating-point precision. Rapier currently uses different crates (rapier2d, rapier2d-f64, rapier3d, rapier3d-f64) for this. This works, but it's not ideal, because it duplicates code.

Of course, you could say that the number type is a sort of backend, the word "backend" can mean a lot of things.

This sounds like something that should be chosen based on the type, not crate-wide.

1 Like

Well, I won't claim that my solution is the only possible one, but I do think it solves that problem thoroughly. If your crate typechecks with all features turned on (but the compiler makes sure it would still compile with any combination of them being turned off), then the features are additive.

I very much relate to this problem. Perhaps a way to enable this sooner rather than later would be to create something new.

Instead of cfg perhaps there could be a pair of macros. One which defined sets of properties like maybe:

cfg_enum!( 
    MyOption {
        Foo,
        Bar,
        Baz,
        Bat,
    }
);

and then another that used them:

#cfg_match(MyOption) {
   Foo => {
      //FooStuff...
   }
   Bar => {
      //BarStuff...
   }
   Baz => {
      //BazStuff...
   }
   Bat => {
      //BatStuff...
   }
};

But where there is a compile time check that every time MyOption is used branches for all of the enumerated values are covered. It ought to be possible to do most types of checks for all the branches even if not compiling for the specific feature or architecture.