Skip to content

RFC: Allow cfg-attributes on elements of tuple type declarations #3532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
323 changes: 323 additions & 0 deletions text/3532-cfg-attribute-in-tuple-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
- Feature Name: `cfg_attribute_in_tuple_type`
- Start Date: 2023-11-23
- RFC PR: [rust-lang/rfcs#3532](https://github.com/rust-lang/rfcs/pull/3532)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

Let's make it more elegant to conditionally compile tuple type declarations by allowing cfg-attributes directly on their element types.

# Motivation
[motivation]: #motivation

### Consistency

Currently, there is limited support for conditionally compiling tuple type declarations:

```rust
type ConditionalTuple = (u32, i32, #[cfg(feature = "foo")] u8);
```

```
error: expected type, found `#`
--> <source>:1:36
|
1 | type ConditionalTuple = (u32, i32, #[cfg(feature = "foo")] u8);
| ^ expected type
```

As with [RFC #3399](https://rust-lang.github.io/rfcs/3399-cfg-attribute-in-where.html), some workarounds exist, but they can result in combinatorial boilerplate:

```rust
// GOAL:
// type ConditionalTuple = (
// u32,
// i32,
// #[cfg(feature = "foo")] u8,
// #[cfg(feature = "bar")] i8,
// );

// CURRENT:
#[cfg(all(feature = "foo", feature = "bar"))]
type ConditionalTuple = (u32, i32, u8, i8);
#[cfg(all(feature = "foo", not(feature = "bar")))]
type ConditionalTuple = (u32, i32, u8);
#[cfg(all(not(feature = "foo"), feature = "bar"))]
type ConditionalTuple = (u32, i32, i8);
#[cfg(all(not(feature = "foo"), not(feature = "bar")))]
type ConditionalTuple = (u32, i32);
```

Rust already supports per-element cfg-attributes in tuple *initialization*. The following is legal Rust code and functions as expected, even though the resulting type of `x` can't be expressed very easily:

```rust
pub fn main() {
let x = (1u32, 4i32, #[cfg(all())] 23u8);
println!("{}", x.2) // Output: 23
}
```

Similarly, cfg-attributes are permitted on types in tuple structs, like so:

```rust
pub struct SomeStruct(u32, #[cfg(feature = "foo")] bool);
```

So it makes sense to support it in regular tuple type declaration as well.

### Use Cases

While structs support cfg-attributes on their members, tuples serve an important purpose in a number of applications that can't easily be replicated with structs. One common example is for achieving variadic-like behavior for constructing and accessing struct-of-array (SoA) data structures. These data structures break large data blocks into modular data components in individual contiguous memory blocks for reusable composition and optimizations via SIMD and improved cache behavior. This is especially prevalent in Entity Component System libraries like [bevy](https://docs.rs/bevy_ecs) and [hecs](https://docs.rs/hecs). For example, to perform a world query in hecs, the user constructs an iterator using a type-tuple like so:

```rust
for (id, (number, &flag)) in world.query_mut::<(&mut i32, &bool)>() {
if flag { *number *= 2; }
}
```

Tuples have a number of unique advantages in this paradigm. For one, they avoid boilerplate due to their ability to be anonymously constructed on the fly. Additionally, tuples can be concatenated and joined (e.g. via the [tuple](https://docs.rs/tuple) crate). This allows more advanced ECS libraries and other similar tools to provide support for pre-determined bundles of components, or use tuple nesting to group logic and functionality. One could theoretically define functions like `query_mut1<T0>`, `query_mut2<T0, T1>`, and so on, but the ergonomics of the tuple approach win out in practice.

In this situation, cfg-attributes come into play when building ECS archetypes (a pre-determined collection of components for a type of entity) for different platforms or deployment targets. Say for example that we were creating a multiplayer asteroids game in an Entity Component System. If we wanted to statically define our archetypes at compile-time (as is the case in [gecs](https://docs.rs/gecs)), it might look something like this:

```rust
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
SpriteComponent,
AudioComponent,
)>;
```

Since this is a multiplayer game, we may want some components to exist solely on the server or on the client, both for security reasons and also for optimization or performance reasons. The sprite and audio components for example serve no purpose on the server as the server does not render graphics or play audio. In games in other languages, it is common practice to use conditional compilation to avoid putting code in various build targets that serve no purpose, waste resources, or potentially leak information to cheaters. So in this case we will restrict these two components to the `client` feature, like so:

```rust
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
#[cfg(feature = "client")] SpriteComponent,
#[cfg(feature = "client")] AudioComponent,
)>;
```

Additionally, we need some components to handle serializing the network state, performing dead reckoning, and sending that information to the client from the server. So we will add a `StateStorageComponent` and a `DeltaCompressionComponent`, and restrict those to the server, since the client does not perform these calculations and we want to avoid giving clients this information in order to help confound cheaters.

```rust
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
#[cfg(feature = "client")] SpriteComponent,
#[cfg(feature = "client")] AudioComponent,
#[cfg(feature = "server")] StateStorageComponent,
#[cfg(feature = "server")] DeltaCompressionComponent,
)>;
```

Finally, we want some debug information for diagnosing physics and damage calculation issues. We build a component to store this intermediate data, but we don't want to ship it in the final game because it's just for aid in development. We'll create a `DebugDrawComponent` and add it to our ship archetype as well, but only when the game is built in editor mode because it's quite expensive to do these extra calculations and draw debug information every frame.

```rust
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
#[cfg(feature = "client")] SpriteComponent,
#[cfg(feature = "client")] AudioComponent,
#[cfg(feature = "server")] StateStorageComponent,
#[cfg(feature = "server")] DeltaCompressionComponent,
#[cfg(feature = "editor")] DebugDrawComponent,
)>;
```

This represents our archetype with the various common and situational components based on its build and deployment target. With this decoration each component is decorated with the context in which it appears, and requires no inference or indirection via macros to generate or read. By comparison, here is how this would be written in Rust today, keeping in mind that a build could be any combination of client, server, and editor for development and debugging purposes (akin to Unreal Engine's "play in editor" feature):

```rust
#[cfg(all(feature = "client", feature = "server", feature = "editor"))]
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
SpriteComponent,
AudioComponent,
StateStorageComponent,
DeltaCompressionComponent,
DebugDrawComponent,
)>;

#[cfg(all(not(feature = "client"), feature = "server", feature = "editor"))]
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
StateStorageComponent,
DeltaCompressionComponent,
DebugDrawComponent,
)>;

#[cfg(all(feature = "client", not(feature = "server"), feature = "editor"))]
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
SpriteComponent,
AudioComponent,
DebugDrawComponent,
)>;

#[cfg(all(not(feature = "client"), not(feature = "server"), feature = "editor"))]
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
DebugDrawComponent,
)>;

#[cfg(all(feature = "client", feature = "server", not(feature = "editor")))]
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
SpriteComponent,
AudioComponent,
StateStorageComponent,
DeltaCompressionComponent,
)>;

#[cfg(all(not(feature = "client"), feature = "server", not(feature = "editor")))]
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
StateStorageComponent,
DeltaCompressionComponent,
)>;

#[cfg(all(feature = "client", not(feature = "server"), not(feature = "editor")))]
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
SpriteComponent,
AudioComponent,
)>;

#[cfg(all(not(feature = "client"), not(feature = "server"), not(feature = "editor")))]
type ShipArchetype = EcsArchetype<(
TransformComponent,
VelocityComponent,
PhysicsComponent,
ColliderComponent,
EngineComponent,
HealthComponent,
WeaponComponent,
EnergyComponent,
)>;
```

This would likely need to be generated via macro in practice, and the macro itself would have to parse the cfg-attributes to produce these combinatorial outputs. However, macros aren't an easy fix in all positions where tuples are supported (e.g. as type arguments), and so even with macros this would create levels of indirection and require alias definitions. The hecs query example above could not easily have an element conditionally gated via a macro without first declaring an alias for that query's tuple type outside of the position where the query iteration occurs. This is because doing so would likely require the macro to be able to generate code outside of its immediate context to function (i.e. to branch based on each cfg-attribute involved).

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Tuple type declarations can use cfg-attributes on individual elements, like so:

```rust
type MyTuple = (
SomeTypeA,
#[cfg(something_a)] SomeTypeB,
#[cfg(something_b)] SomeTypeC,
)
```

and in other situations where tuple types are declared, such as in function arguments. These will conditionally include or exclude the type in that tuple (affecting the tuple's length) based on the compile-time evaluation result of each `#[cfg]` predicate.

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

This RFC proposes changing the syntax of the `TupleType` (see 10.1.5 in the Rust reference) to include `OuterAttribute*` before each occurrence of `Type`. These attributes can decorate each individual type (up to the comma or closing paren). In practice, at least within the scope of this RFC, only cfg-attributes need to be supported in this position.

# Drawbacks
[drawbacks]: #drawbacks

As with any feature, this adds complication to the language and grammar. Conditionally compiling tuple type elements can be a semver breaking change, but not any more than with the already existing workarounds.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

(See [RFC #3399](https://rust-lang.github.io/rfcs/3399-cfg-attribute-in-where.html) for a similar write-up.)

The need for conditionally compiling tuple types can arise in applications with different deployment targets or that want to release builds with different sets of functionality (e.g. client, server, editor, demo, etc.). It would be useful to support cfg-attributes directly here without requiring workarounds to achieve this functionality. Macros, proc macros, and so on are also ways to conditionally compile tuple types, but these also introduce at least one level of obfuscation from the core goal and can't be used everywhere a tuple can be. Finally, tuples can be wholly duplicated under different cfg-attributes, but this scales poorly with both the size and intricacy of the tuple and the number of interacting attributes (which may grow combinatorically), and can introduce a maintenance burden from repeated code.

It also makes sense in this instance to support cfg-attributes here because they are already supported in this manner for tuple initialization and for tuple struct declaration.

# Prior art
[prior-art]: #prior-art

I'm not aware of any prior work in adding this to the language.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

I don't have any unresolved questions for this RFC.

# Future possibilities
[future-possibilities]: #future-possibilities

I believe this change is relatively self-contained, though I also think it's worth continuing to look for additional places where support for cfg-attributes makes sense to add. Conditional compilation is very important, especially in some domains, and requiring workarounds and additional boilerplate to support it is not ideal.