Skip to content

Commit 2d69307

Browse files
committed
Merge remote-tracking branch 'eddyb/const-fn'
2 parents e6715f0 + 6465748 commit 2d69307

File tree

1 file changed

+216
-0
lines changed

1 file changed

+216
-0
lines changed

text/0000-const-fn.md

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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

Comments
 (0)