|
| 1 | +- Feature Name: const_fn |
| 2 | +- Start Date: 2015-02-25 |
| 3 | +- RFC PR: (leave this empty) |
| 4 | +- Rust Issue: (leave this empty) |
| 5 | + |
| 6 | +# Summary |
| 7 | + |
| 8 | +Allow marking free functions and inherent methods as `const`, enabling them to be |
| 9 | +called in constants contexts, with constant arguments. |
| 10 | + |
| 11 | +# Motivation |
| 12 | + |
| 13 | +As it is right now, `UnsafeCell` is a stabilization and safety hazard: the field |
| 14 | +it is supposed to be wrapping is public. This is only done out of the necessity |
| 15 | +to initialize static items containing atomics, mutexes, etc. - for example: |
| 16 | +```rust |
| 17 | +#[lang="unsafe_cell"] |
| 18 | +struct UnsafeCell<T> { pub value: T } |
| 19 | +struct AtomicUsize { v: UnsafeCell<usize> } |
| 20 | +const ATOMIC_USIZE_INIT: AtomicUsize = AtomicUsize { |
| 21 | + v: UnsafeCell { value: 0 } |
| 22 | +}; |
| 23 | +``` |
| 24 | + |
| 25 | +This approach is fragile and doesn't compose well - consider having to initialize |
| 26 | +an `AtomicUsize` static with `usize::MAX` - you would need a `const` for each |
| 27 | +possible value. |
| 28 | + |
| 29 | +Also, types like `AtomicPtr<T>` or `Cell<T>` have no way *at all* to initialize |
| 30 | +them in constant contexts, leading to overuse of `UnsafeCell` or `static mut`, |
| 31 | +disregarding type safety and proper abstractions. |
| 32 | + |
| 33 | +During implementation, the worst offender I've found was `std::thread_local`: |
| 34 | +all the fields of `std::thread_local::imp::Key` are public, so they can be |
| 35 | +filled in by a macro - and they're also marked "stable" (due to the lack of |
| 36 | +stability hygiene in macros). |
| 37 | + |
| 38 | +A pre-RFC for the removal of the dangerous (and oftenly misued) `static mut` |
| 39 | +received positive feedback, but only under the condition that abstractions |
| 40 | +could be created and used in `const` and `static` items. |
| 41 | + |
| 42 | +Another concern is the ability to use certain intrinsics, like `size_of`, inside |
| 43 | +constant expressions, including fixed-length array types. Unlike keyword-based |
| 44 | +alternatives, `const fn` provides an extensible and composable building block |
| 45 | +for such features. |
| 46 | + |
| 47 | +The design should be as simple as it can be, while keeping enough functionality |
| 48 | +to solve the issues mentioned above. |
| 49 | +The intention is to have something usable at 1.0 without limiting what we can |
| 50 | +in the future. Compile-time pure constants (the existing `const` items) with |
| 51 | +added parametrization over types and values (arguments) should suffice. |
| 52 | + |
| 53 | +# Detailed design |
| 54 | + |
| 55 | +Functions and inherent methods can be marked as `const`: |
| 56 | +```rust |
| 57 | +const fn foo(x: T, y: U) -> Foo { |
| 58 | + stmts; |
| 59 | + expr |
| 60 | +} |
| 61 | +impl Foo { |
| 62 | + const fn new(x: T) -> Foo { |
| 63 | + stmts; |
| 64 | + expr |
| 65 | + } |
| 66 | + |
| 67 | + const fn transform(self, y: U) -> Foo { |
| 68 | + stmts; |
| 69 | + expr |
| 70 | + } |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +Traits, trait implementations and their methods cannot be `const` - this |
| 75 | +allows us to properly design a constness/CTFE system that interacts well |
| 76 | +with traits - for more details, see *Alternatives*. |
| 77 | + |
| 78 | +Only simple by-value bindings are allowed in arguments, e.g. `x: T`. While |
| 79 | +by-ref bindings and destructuring can be supported, they're not necessary |
| 80 | +and they would only complicate the implementation. |
| 81 | + |
| 82 | +The body of the function is checked as if it were a block inside a `const`: |
| 83 | +```rust |
| 84 | +const FOO: Foo = { |
| 85 | + // Currently, only item "statements" are allowed here. |
| 86 | + stmts; |
| 87 | + // The function's arguments and constant expressions can be freely combined. |
| 88 | + expr |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +As the current `const` items are not formally specified (yet), there is a need |
| 93 | +to expand on the rules for `const` values (pure compile-time constants), instead |
| 94 | +of leaving them implicit: |
| 95 | +* the set of currently implemented expressions is: primitive literals, ADTs |
| 96 | +(tuples, arrays, structs, enum variants), unary/binary operations on primitives, |
| 97 | +casts, field accesses/indexing, capture-less closures, references and blocks |
| 98 | +(only item statements and a tail expression) |
| 99 | +* no side-effects (assignments, non-`const` function calls, inline assembly) |
| 100 | +* struct/enum values are not allowed if their type implements `Drop`, but |
| 101 | +this is not transitive, allowing the (perfectly harmless) creation of, e.g. |
| 102 | +`None::<Vec<T>>` (as an aside, this rule could be used to allow `[x; N]` even |
| 103 | +for non-`Copy` types of `x`, but that is out of the scope of this RFC) |
| 104 | +* references are trully immutable, no value with interior mutability can be placed |
| 105 | +behind a reference, and mutable references can only be created from zero-sized |
| 106 | +values (e.g. `&mut || {}`) - this allows a reference to be represented just by |
| 107 | +its value, with no guarantees for the actual address in memory |
| 108 | +* raw pointers can only be created from an integer, a reference or another raw |
| 109 | +pointer, and cannot be dereferenced or cast back to an integer, which means any |
| 110 | +constant raw pointer can be represented by either a constant integer or reference |
| 111 | +* as a result of not having any side-effects, loops would only affect termination, |
| 112 | +which has no practical value, thus remaining unimplemented |
| 113 | +* although more useful than loops, conditional control flow (`if`/`else` and |
| 114 | +`match`) also remains unimplemented and only `match` would pose a challenge |
| 115 | +* immutable `let` bindings in blocks have the same status and implementation |
| 116 | +difficulty as `if`/`else` and they both suffer from a lack of demand (blocks |
| 117 | +were originally introduced to `const`/`static` for scoping items used only in |
| 118 | +the initializer of a global). |
| 119 | + |
| 120 | +For the purpose of rvalue promotion (to static memory), arguments are considered |
| 121 | +potentially varying, because the function can still be called with non-constant |
| 122 | +values at runtime. |
| 123 | + |
| 124 | +`const` functions and methods can be called from any constant expression: |
| 125 | +```rust |
| 126 | +// Standalone example. |
| 127 | +struct Point { x: i32, y: i32 } |
| 128 | + |
| 129 | +impl Point { |
| 130 | + const fn new(x: i32, y: i32) -> Point { |
| 131 | + Point { x: x, y: y } |
| 132 | + } |
| 133 | + |
| 134 | + const fn add(self, other: Point) -> Point { |
| 135 | + Point::new(self.x + other.x, self.y + other.y) |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +const ORIGIN: Point = Point::new(0, 0); |
| 140 | + |
| 141 | +const fn sum_test(xs: [Point; 3]) -> Point { |
| 142 | + xs[0].add(xs[1]).add(xs[2]) |
| 143 | +} |
| 144 | + |
| 145 | +const A: Point = Point::new(1, 0); |
| 146 | +const B: Point = Point::new(0, 1); |
| 147 | +const C: Point = A.add(B); |
| 148 | +const D: Point = sum_test([A, B, C]); |
| 149 | + |
| 150 | +// Assuming the Foo::new methods used here are const. |
| 151 | +static FLAG: AtomicBool = AtomicBool::new(true); |
| 152 | +static COUNTDOWN: AtomicUsize = AtomicUsize::new(10); |
| 153 | +#[thread_local] |
| 154 | +static TLS_COUNTER: Cell<u32> = Cell::new(1); |
| 155 | +``` |
| 156 | + |
| 157 | +Type parameters and their bounds are not restricted, though trait methods cannot |
| 158 | +be called, as they are never `const` in this design. Accessing trait methods can |
| 159 | +still be useful - for example, they can be turned into function pointers: |
| 160 | +```rust |
| 161 | +const fn arithmetic_ops<T: Int>() -> [fn(T, T) -> T; 4] { |
| 162 | + [Add::add, Sub::sub, Mul::mul, Div::div] |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +# Drawbacks |
| 167 | + |
| 168 | +* A design that is not conservative enough risks creating backwards compatibility |
| 169 | +hazards that might only be uncovered when a more extensive CTFE proposal is made, |
| 170 | +after 1.0. |
| 171 | + |
| 172 | +# Alternatives |
| 173 | + |
| 174 | +* Not do anything for 1.0. This would result in some APIs being crippled and |
| 175 | +serious backwards compatibility issues - `UnsafeCell`'s `value` field cannot |
| 176 | +simply be removed later. |
| 177 | +* While not an alternative, but rather a potential extension, I want to point |
| 178 | +out there is only way I could make `const fn`s work with traits (in an untested |
| 179 | +design, that is): qualify trait implementations and bounds with `const`. |
| 180 | +This is necessary for meaningful interactions with operator overloading traits: |
| 181 | +```rust |
| 182 | +const fn map_vec3<T: Copy, F: const Fn(T) -> T>(xs: [T; 3], f: F) -> [T; 3] { |
| 183 | + [f([xs[0]), f([xs[1]), f([xs[2])] |
| 184 | +} |
| 185 | + |
| 186 | +const fn neg_vec3<T: Copy + const Neg>(xs: [T; 3]) -> [T; 3] { |
| 187 | + map_vec3(xs, |x| -x) |
| 188 | +} |
| 189 | + |
| 190 | +const impl Add for Point { |
| 191 | + fn add(self, other: Point) -> Point { |
| 192 | + Point { |
| 193 | + x: self.x + other.x, |
| 194 | + y: self.y + other.y |
| 195 | + } |
| 196 | + } |
| 197 | +} |
| 198 | +``` |
| 199 | +Having `const` trait methods (where all implementations are `const`) seems |
| 200 | +useful, but it would not allow the usecase above on its own. |
| 201 | +Trait implementations with `const` methods (instead of the entire `impl` |
| 202 | +being `const`) would allow direct calls, but it's not obvious how one could |
| 203 | +write a function generic over a type which implements a trait and requiring |
| 204 | +that a certain method of that trait is implemented as `const`. |
| 205 | + |
| 206 | +# Unresolved questions |
| 207 | + |
| 208 | +* Allow `unsafe const fn`? The implementation cost is negligible, but I am not |
| 209 | +certain it needs to exist. |
| 210 | +* Keep recursion or disallow it for now? The conservative choice of having no |
| 211 | +recursive `const fn`s would not affect the usecases intended for this RFC. |
| 212 | +If we do allow it, we probably need a recursion limit, and/or an evaluation |
| 213 | +algorithm that can handle *at least* tail recursion. |
| 214 | +Also, there is no way to actually write a recursive `const fn` at this moment, |
| 215 | +because no control flow primitives are implemented for constants, but that |
| 216 | +cannot be taken for granted, at least `if`/`else` should eventually work. |
0 commit comments