|
| 1 | +- Feature Name: `static_in_pattern` |
| 2 | +- Start Date: 2022-08-17 |
| 3 | +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) |
| 4 | +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) |
| 5 | + |
| 6 | +# Summary |
| 7 | + |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +Allow referencing non-`mut` `static`s in pattern matches wherever referencing a `const` of the same type would be allowed. |
| 11 | + |
| 12 | +# Motivation |
| 13 | + |
| 14 | +[motivation]: #motivation |
| 15 | + |
| 16 | +Rust pattern matches compare a scrutinee against compile-time information. Rust generally doesn't allow patterns to depend on runtime information; that is relegated to match guards. However, Rust currently also prevents patterns from depending on *link-time* information, specifically statics from `extern` blocks. |
| 17 | + |
| 18 | +I encountered this restriction while trying to port the Rust standard library to [cosmopolitan libc](https://justine.lol/cosmopolitan/index.html). Cosmopolitan provides an API that mostly matches POSIX, with one major exception: constants like `ENOSYS` and `EINVAL`, which on most platforms are defined as C `#define`s (equivalent to Rust `const`s), are instead provided as C `const`s (equivalent to Rust non-`mut` `static`s). |
| 19 | + |
| 20 | +```rust |
| 21 | +// libc crate |
| 22 | + |
| 23 | +cfg_if! { |
| 24 | + if #[cfg(target_env = "cosmopolitan")] { |
| 25 | + extern "C" { |
| 26 | + pub static EINVAL: i32; |
| 27 | + pub static ENOSYS: i32; |
| 28 | + pub static ENOENT: i32; |
| 29 | + } |
| 30 | + } else { |
| 31 | + pub const EINVAL: i32 = 42; |
| 32 | + pub const ENOSYS: i32 = 43; |
| 33 | + pub const ENOENT: i32 = 44; |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +// stdlib code |
| 38 | + |
| 39 | +use libc::*; |
| 40 | + |
| 41 | +fn process_error(error_code: i32) { |
| 42 | + match error_code { |
| 43 | + // Compiler throws error EO530 on Cosmopolitan, |
| 44 | + // because `static`s can't be used in patterns, only `const`s |
| 45 | + EINVAL => do_stuff(), |
| 46 | + ENOSYS => panic!("oh noes"), |
| 47 | + ENOENT => make_it_work(), |
| 48 | + _ => do_different_stuff(), |
| 49 | + } |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +Because Rust patterns don't support statics, all the `match` expressions in the standard library that refer to POSIX constants would currently need to be rewritten to accommodate Cosmopolitan. |
| 54 | + |
| 55 | +```rust |
| 56 | +// stdlib code adapted for cosmopolitan |
| 57 | + |
| 58 | +use libc::*; |
| 59 | + |
| 60 | +fn process_error(error_code: i32) { |
| 61 | + if error_code == EINVAL { |
| 62 | + do_stuff(); |
| 63 | + } else if error_code == ENOSYS { |
| 64 | + panic!("oh noes"); |
| 65 | + } else if error_code == ENOENT { |
| 66 | + make_it_work(); |
| 67 | + } else { |
| 68 | + do_different_stuff(); |
| 69 | + } |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +Needless to say, this is unlikely to ever be upstreamed. Allowing statics in patterns would solve this use-case much more cleanly. |
| 74 | + |
| 75 | +# Guide-level explanation |
| 76 | + |
| 77 | +[guide-level-explanation]: #guide-level-explanation |
| 78 | + |
| 79 | +Rust patterns can refer to constants. |
| 80 | + |
| 81 | +```rust |
| 82 | +const EVERYTHING: i32 = 42; |
| 83 | + |
| 84 | +fn foo(scrutinee: i32) { |
| 85 | + match scrutinee { |
| 86 | + EVERYTHING => println!("have all of it"), |
| 87 | + _ => println!("need moar"), |
| 88 | + } |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +With this feature, they can refer to statics as well. |
| 93 | + |
| 94 | +```rust |
| 95 | +static EVERYTHING: i32 = 42; |
| 96 | + |
| 97 | +fn foo(scrutinee: i32) { |
| 98 | + match scrutinee { |
| 99 | + EVERYTHING => println!("have all of it"), |
| 100 | + _ => println!("need moar"), |
| 101 | + } |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +Mutable statics are not allowed, however. Patterns can't reference information that can change at runtime, and also can't be `unsafe`. |
| 106 | + |
| 107 | +```rust |
| 108 | + |
| 109 | +static mut EVERYTHING: i32 = 42; |
| 110 | + |
| 111 | +fn foo(scrutinee: i32) { |
| 112 | + match scrutinee { |
| 113 | + // ERROR can't refer to mutable statics in patterns |
| 114 | + /* EVERYTHING => println!("have all of it"), */ |
| 115 | + _ => println!("need moar"), |
| 116 | + } |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +Statics from `extern` blocks are allowed, but they must be marked as trusted using the (not-yet-implemented) [trusted external statics](https://github.com/rust-lang/lang-team/issues/149) feature. |
| 121 | + |
| 122 | +```rust |
| 123 | +extern "C" { |
| 124 | + #[unsafe(trusted_extern)] |
| 125 | + static EVERYTHING: i32; |
| 126 | +} |
| 127 | + |
| 128 | +fn foo(scrutinee: i32) { |
| 129 | + match scrutinee { |
| 130 | + EVERYTHING => println!("have all of it"), |
| 131 | + _ => println!("need moar"), |
| 132 | + } |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +# Reference-level explanation |
| 137 | + |
| 138 | +[reference-level-explanation]: #reference-level-explanation |
| 139 | + |
| 140 | +For a static to be eligible for use in a pattern, it must: |
| 141 | + |
| 142 | +- not be marked `mut` |
| 143 | +- not be marked `#[thread_local]` |
| 144 | +- not come from an extern block, unless it is marked as safe to use with the [trusted external statics](https://github.com/rust-lang/lang-team/issues/149) feature |
| 145 | +- have a type that satisfies the structural match rules, as described in [RFC 1445](1445-restrict-constants-in-patterns.md), but without any allowances for backward compatibility like there are for consts (e.g., floating point numbers in patterns) . These rules exclude all statics with interior mutability. |
| 146 | + |
| 147 | +Static patterns match exactly when a const pattern with a const of the same type and value would match. |
| 148 | + |
| 149 | +The values of statics are treated as opaque for reachability and exhaustiveness analysis. |
| 150 | + |
| 151 | +```rust |
| 152 | +static TRUE: bool = true; |
| 153 | +static FALSE: bool = false; |
| 154 | + |
| 155 | +fn foo(scrutinee: bool) { |
| 156 | + match scrutinee { |
| 157 | + TRUE | FALSE => println!("bar"), |
| 158 | + |
| 159 | + // The compiler will throw an error if you remove this branch; |
| 160 | + // it is not allowed to look into the values of the statics |
| 161 | + // to determine that it is unreachable. |
| 162 | + _ => println!("baz"), |
| 163 | + } |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +As an exception, when all safe values of a type are structurally equal, the compiler is allowed to see that the match will always succeed. |
| 168 | + |
| 169 | +```rust |
| 170 | +// Not all `&()` are bitwise equal, |
| 171 | +// but they are structurally equal, |
| 172 | +// that is what matters. |
| 173 | +static ONE_TRUE_VALUE: &() = &(); |
| 174 | + |
| 175 | +fn foo(scrutinee: &()) { |
| 176 | + match scrutinee { |
| 177 | + ONE_TRUE_VALUE => println!("only one branch"), |
| 178 | + // No need for a wildcard. |
| 179 | + // The above match always succeeds. |
| 180 | + } |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +Visibility and `#[non_exhaustive]` can affect whether the compiler can tell that all values of the type are structurally equal. |
| 185 | + |
| 186 | +```rust |
| 187 | +mod stuff { |
| 188 | + #[derive(PartialEq, Eq)] |
| 189 | + pub(super) struct PrivateZst(()); |
| 190 | + |
| 191 | + pub(super) static PRIVATE_ZST: PrivateZst = PrivateZst(()); |
| 192 | +} |
| 193 | + |
| 194 | +fn foo(scrutinee: stuff::PrivateZst) { |
| 195 | + match scrutinee { |
| 196 | + stuff::PRIVATE_ZST => println!("secrets abound"), |
| 197 | + // `stuff::PrivateZst` has a field that's not visible in this scope, |
| 198 | + // so we can't tell that all values are equivalent. |
| 199 | + // The wildcard branch is required. |
| 200 | + _ => println!("incorrect password"), |
| 201 | + } |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +```rust |
| 206 | +// crate `stuff` |
| 207 | +#[derive(PartialEq, Eq)] |
| 208 | +#[non_exhaustive] |
| 209 | +struct PrivateZst(); |
| 210 | + |
| 211 | +// main crate |
| 212 | +extern crate stuff; |
| 213 | + |
| 214 | +fn foo(scrutinee: stuff::PrivateZst) { |
| 215 | + match scrutinee { |
| 216 | + stuff::PRIVATE_ZST => println!("secrets abound"), |
| 217 | + // `stuff::PrivateZst` is marked `#[non_exhaustive]` |
| 218 | + // and comes from an external crate, |
| 219 | + // so we can't tell that all values are equivalent. |
| 220 | + // The wildcard branch is required. |
| 221 | + _ => println!("incorrect password"), |
| 222 | + } |
| 223 | +} |
| 224 | +``` |
| 225 | + |
| 226 | +The examples above all use `match`, but statics would be allowed in all other language constructs that use patterns, including `let`, `if let`, and function parameters. However, as statics cannot be used in const contexts, static patterns are be unavailable there as well. |
| 227 | + |
| 228 | +# Drawbacks |
| 229 | + |
| 230 | +[drawbacks]: #drawbacks |
| 231 | + |
| 232 | +This change slightly weakens the rule that patterns can only rely on compile-time information. In addition, static patterns may have slightly worse performance than the equivalent constant patterns. |
| 233 | + |
| 234 | +# Rationale and alternatives |
| 235 | + |
| 236 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 237 | + |
| 238 | +The proposed rules around reachability and exhaustiveness checking are designed to ensure that changing the value of a static, or changing from a static defined in Rust to a trusted extern static, is never a breaking change. The special dispensations for types with a single value could be considered unnecessary, as matching on such a type is a pointless operation. However, the rules are not difficult to implement (I managed to do it myself, despite near-zero experience contributing to the compiler), and are arguably the most correct and least surprising semantics. |
| 239 | + |
| 240 | +Allowing unsafe-to-access statics in patterns (`static mut`s, untrusted `extern` statics, `#[thread_local]` statics) is another possibility. However, I believe this option to be unwise: |
| 241 | + |
| 242 | +- Rust generally has not allowed unsafe operations (like union field accesses) in pattern matches |
| 243 | +- It's not clear where the `unsafe` keyword would go (within the pattern? around the whole `match` or `let`? what about patterns in function parameters?) |
| 244 | +- it requires Rust to commit to and document, and users to understand, when exactly it is allowed to dereference the static when performing a pattern match |
| 245 | + |
| 246 | +As for not making this change at all, I believe this would be a loss for the language as it would lock out the use-cases described above. This is a very simple feature, it doesn't conflict with any other potential extensions, the behavior and syntax fit well with the rest of the language, and it is immediately understandable to anyone who is already familiar with matching on `const`s. |
| 247 | + |
| 248 | +# Prior art |
| 249 | + |
| 250 | +[prior-art]: #prior-art |
| 251 | + |
| 252 | +As far as I am aware, no other language has an analogous feature. C's `switch` statement does not allow referring to C `const`s. |
| 253 | + |
| 254 | +# Unresolved questions |
| 255 | + |
| 256 | +[unresolved-questions]: #unresolved-questions |
| 257 | + |
| 258 | +The motivation for this RFC assumes that [trusted external statics](https://github.com/rust-lang/lang-team/issues/149) will eventually be implemented and stabilized. Other than that, there are no unresolved questions that I am aware of. |
| 259 | + |
| 260 | +# Future possibilities |
| 261 | + |
| 262 | +[future-possibilities]: #future-possibilities |
| 263 | + |
| 264 | +None; this is a very simple and self-contained feature. I've argued against some possible extensions in the [rationale and alternatives](#rationale-and-alternatives) section. Future changes to the structural equality rules might affect this feature, but that is anther discussion and out of scope for this RFC. |
0 commit comments