|
| 1 | +- Feature Name: `arbitrary_enum_discriminant` |
| 2 | +- Start Date: 2018-03-11 |
| 3 | +- RFC PR: [rust-lang/rfcs#2363](https://github.com/rust-lang/rfcs/pull/2363) |
| 4 | +- Rust Issue: [rust-lang/rust#60553](https://github.com/rust-lang/rust/issues/60553) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +This RFC gives users a way to control the discriminants of variants of all |
| 10 | +enumerations, not just the ones that are shaped like C-like enums (i.e. where |
| 11 | +all the variants have no fields). |
| 12 | + |
| 13 | +The change is minimal: allow any variant to be adorned with an explicit |
| 14 | +discriminant value, whether or not that variant has any field. |
| 15 | + |
| 16 | +# Motivation |
| 17 | +[motivation]: #motivation |
| 18 | + |
| 19 | +Stylo, the style system of Servo, represents CSS properties with a large |
| 20 | +enumeration `PropertyDeclaration` where each variant has only one field which |
| 21 | +represents the value of a given CSS property. Here is a subset of it: |
| 22 | + |
| 23 | +```rust |
| 24 | +#[repr(u16)] |
| 25 | +enum PropertyDeclaration { |
| 26 | + Color(Color), |
| 27 | + Height(Length), |
| 28 | + InlineSize(Length), |
| 29 | + TransformOrigin(TransformOrigin), |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +For various book-keeping reasons, Servo also generates a `LonghandId` |
| 34 | +enumeration with the same variants as `PropertyDeclaration` but without the |
| 35 | +fields, thus making `LonghandId` a C-like enumeration: |
| 36 | + |
| 37 | +```rust |
| 38 | +#[derive(Clone, Copy)] |
| 39 | +#[repr(u16)] |
| 40 | +enum LonghandId { |
| 41 | + Color, |
| 42 | + Height, |
| 43 | + InlineSize, |
| 44 | + TransformOrigin, |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +Given that rustc guarantees that `#[repr(u16)]` enumerations start with their |
| 49 | +discriminant stored as a `u16`, going from `&PropertyDeclaration` to |
| 50 | +`LonghandId` is then just a matter of unsafely coercing `&self` as a |
| 51 | +`&LonghandId`: |
| 52 | + |
| 53 | +```rust |
| 54 | +impl PropertyDeclaration { |
| 55 | + fn id(&self) -> LonghandId { |
| 56 | + unsafe { *(self as *const Self as *const LonghandId) } |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +This works great, but doesn't scale if we want to replicate this behaviour for |
| 62 | +an enumeration that is a subset of `PropertyDeclaration`, for example an |
| 63 | +enumeration `AnimationValue` that is limited to animatable properties: |
| 64 | + |
| 65 | +```rust |
| 66 | +#[repr(u16)] |
| 67 | +enum AnimationValue { |
| 68 | + Color(Color), |
| 69 | + Height(Length), |
| 70 | + TransformOrigin(TransformOrigin), |
| 71 | +} |
| 72 | + |
| 73 | +impl AnimationValue { |
| 74 | + fn id(&self) -> LonghandId { |
| 75 | + // We can't just unsafely read `&self` as a `&LonghandId` because |
| 76 | + // the discriminant of `AnimationValue::TransformOrigin` isn't equal |
| 77 | + // to `LonghandId::TransformOrigin` anymore. |
| 78 | + match *self { |
| 79 | + AnimationValue::Color(_) => LonghandId::Color, |
| 80 | + AnimationValue::Height(_) => LonghandId::Height, |
| 81 | + AnimationValue::TransformOrigin(_) => LonghandId::TransformOrigin, |
| 82 | + } |
| 83 | + } |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +This is not sustainable, as the jump table generated by rustc to compile this |
| 88 | +huge match expression is larger than 4KB in the final Gecko binary, when this |
| 89 | +operation could be a trivial `u16` copy. This is worked around in Servo by |
| 90 | +generating spurious `Void` variants for the non-animatable properties in |
| 91 | +`AnimationValue`: |
| 92 | + |
| 93 | +```rust |
| 94 | +enum Void {} |
| 95 | + |
| 96 | +#[repr(u16)] |
| 97 | +enum AnimationValue { |
| 98 | + Color(Color), |
| 99 | + Height(Length), |
| 100 | + InlineSize(Void), |
| 101 | + TransformOrigin(TransformOrigin), |
| 102 | +} |
| 103 | + |
| 104 | +impl AnimationValue { |
| 105 | + fn id(&self) -> LonghandId |
| 106 | + // We can use the unsafe trick again. |
| 107 | + unsafe { *(self as *const Self as *const LonghandId) } |
| 108 | + } |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +This is unfortunately quite painful to use, given now all methods matching |
| 113 | +against `AnimationValue` need to have dummy arms for all of these variants: |
| 114 | + |
| 115 | +```rust |
| 116 | +impl AnimationValue { |
| 117 | + fn do_something(&self) { |
| 118 | + match *self { |
| 119 | + AnimationValue::Color(ref color) => { |
| 120 | + do_something_with_color(color) |
| 121 | + } |
| 122 | + AnimationValue::Height(ref height) => { |
| 123 | + do_something_with_height(height) |
| 124 | + } |
| 125 | + // This shouldn't be needed. |
| 126 | + AnimationValue::InlineSize(ref void) => { |
| 127 | + match *void {} |
| 128 | + } |
| 129 | + AnimationValue::TransformOrigin(ref origin) => { |
| 130 | + do_something_with_transform_origin(origin) |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +We suggest generalising the explicit discriminant notation to all enums, |
| 138 | +regardless of whether their variants have fields or not: |
| 139 | + |
| 140 | +```rust |
| 141 | +#[repr(u16)] |
| 142 | +enum AnimationValue { |
| 143 | + Color(Color) = LonghandId::Color as u16, |
| 144 | + Height(Length) = LonghandId::Height as u16, |
| 145 | + TransformOrigin(TransformOrigin) = LonghandId::TransformOrigin as u16, |
| 146 | +} |
| 147 | + |
| 148 | +impl AnimationValue { |
| 149 | + fn id(&self) -> LonghandId |
| 150 | + // We can use the unsafe trick again. |
| 151 | + unsafe { *(self as *const Self as *const LonghandId) } |
| 152 | + } |
| 153 | + |
| 154 | + fn do_something(&self) { |
| 155 | + // No spurious variant anymore. |
| 156 | + match *self { |
| 157 | + AnimationValue::Color(ref color) => { |
| 158 | + do_something_with_color(color) |
| 159 | + } |
| 160 | + AnimationValue::Height(ref height) => { |
| 161 | + do_something_with_height(height) |
| 162 | + } |
| 163 | + AnimationValue::TransformOrigin(ref origin) => { |
| 164 | + do_something_with_transform_origin(origin) |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +# Guide-level explanation |
| 172 | +[guide-level-explanation]: #guide-level-explanation |
| 173 | + |
| 174 | +An enumeration with only field-less variants can currently have explicit |
| 175 | +discriminant values: |
| 176 | + |
| 177 | +```rust |
| 178 | +enum ForceFromage { |
| 179 | + Emmental = 0, |
| 180 | + Camembert = 1, |
| 181 | + Roquefort = 2, |
| 182 | +} |
| 183 | +``` |
| 184 | + |
| 185 | +With this RFC, users are allowed to put explicit discriminant values on any |
| 186 | +variant of any enumeration, not just the ones where all variants are field-less: |
| 187 | + |
| 188 | +```rust |
| 189 | +enum ParisianSandwichIngredient { |
| 190 | + Bread(BreadKind) = 0, |
| 191 | + Ham(HamKind) = 1, |
| 192 | + Butter(ButterKind) = 2, |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +# Reference-level explanation |
| 197 | +[reference-level-explanation]: #reference-level-explanation |
| 198 | + |
| 199 | +## Grammar |
| 200 | + |
| 201 | +The production for enumeration items becomes: |
| 202 | + |
| 203 | +``` |
| 204 | +EnumItem : |
| 205 | + OuterAttribute* |
| 206 | + IDENTIFIER ( EnumItemTuple | EnumItemStruct)? EnumItemDiscriminant? |
| 207 | +``` |
| 208 | + |
| 209 | +## Semantics |
| 210 | + |
| 211 | +The limitation that only field-less enumerations can have explicit discriminant |
| 212 | +values is lifted, and no other change is made to their semantics: |
| 213 | + |
| 214 | + * enumerations with fields still can't be casted to numeric types |
| 215 | + with the `as` operator; |
| 216 | + * if the first variant doesn't have an explicit discriminant, |
| 217 | + it is set to zero; |
| 218 | + * any unspecified discriminant is set to one higher than the one from |
| 219 | + the previous variant; |
| 220 | + * under the default representation, the specified discriminants are |
| 221 | + interpreted as `isize`; |
| 222 | + * two variants cannot share the same discriminant. |
| 223 | + |
| 224 | +# Drawbacks |
| 225 | +[drawbacks]: #drawbacks |
| 226 | + |
| 227 | +This introduces one more knob to the representation of enumerations. |
| 228 | + |
| 229 | +# Rationale and alternatives |
| 230 | +[alternatives]: #alternatives |
| 231 | + |
| 232 | +Reusing the current syntax and semantics for explicit discriminants of |
| 233 | +field-less enumerations means that the changes to the grammar and semantics of |
| 234 | +the language are minimal. There are a few possible alternatives nonetheless. |
| 235 | + |
| 236 | +## Discriminant values in attributes |
| 237 | + |
| 238 | +We could specify the discriminant values in variant attributes, but this would |
| 239 | +be at odds with the syntax for field-less enumerations. |
| 240 | + |
| 241 | +```rust |
| 242 | +enum ParisianSandwichIngredient { |
| 243 | + #[discriminant = 0] |
| 244 | + Bread(BreadKind), |
| 245 | + #[discriminant = 1] |
| 246 | + Ham(HamKind), |
| 247 | + #[discriminant = 2] |
| 248 | + Butter(ButterKind), |
| 249 | +} |
| 250 | +``` |
| 251 | + |
| 252 | +## Use discriminants of a separate field-less enumeration |
| 253 | + |
| 254 | +We could tell rustc to tie the discriminants of the enumeration to the |
| 255 | +variants of a separate field-less enumeration. |
| 256 | + |
| 257 | +```rust |
| 258 | +#[discriminant(IngredientKind)] |
| 259 | +enum ParisianSandwichIngredient { |
| 260 | + Bread(BreadKind), |
| 261 | + Ham(HamKind), |
| 262 | + Butter(ButterKind), |
| 263 | +} |
| 264 | + |
| 265 | +enum IngredientKind { |
| 266 | + Bread, |
| 267 | + Ham, |
| 268 | + Butter, |
| 269 | +} |
| 270 | +``` |
| 271 | + |
| 272 | +This isn't applicable if such a separate field-less enumeration doesn't exist, |
| 273 | +and this can easily be done as a procedural macro using the feature described |
| 274 | +in this RFC. It also looks way more like spooky action at a distance. |
| 275 | + |
| 276 | +# Prior art |
| 277 | +[prior-art]: #prior-art |
| 278 | + |
| 279 | +No prior art. |
| 280 | + |
| 281 | +# Unresolved questions |
| 282 | +[unresolved]: #unresolved-questions |
| 283 | + |
| 284 | +## Should discriminants of enumerations with fields be specified as variant attributes? |
| 285 | + |
| 286 | +Should they? |
| 287 | + |
| 288 | +## Should this apply only to enumerations with an explicit representation? |
| 289 | + |
| 290 | +Should it? |
| 291 | + |
| 292 | +# Thanks |
| 293 | + |
| 294 | +Thanks to Mazdak Farrokhzad (@Centril) and Simon Sapin (@SimonSapin) for |
| 295 | +the reviews, and my local bakery for their delicious baguettes. 🥖 |
0 commit comments