Description
Current proposal/summary: #20 (comment)
Motivation
In order to totally outdo any other constant evaluators out there, it is desirable to allow things like using serde to deserialize e.g. json or toml files into constants. In order to not duplicate code between const eval and runtime, this will require types like Vec
and String
. Otherwise every type with a String
field would either need to be generic and support &str
and String
in that field, or just outright have a mirror struct for const eval. Both ways seem too restrictive and not in the spirit of "const eval that just works".
Design
Allocating and Deallocating
Allow allocating and deallocating heap inside const eval. This means Vec
, String
, Box
* Similar to how panic
is handled, we intercept calls to an allocator's alloc
method and never actually call that method. Instead the miri-engine runs const eval specific code for producing an allocation that "counts as heap" during const eval, but if it ends up in the final constant, it becomes an unnamed static. If it is leaked without any leftover references to it, the value simply disappears after const eval is finished. If the value is deallocated, the call to dealloc
in intercepted and the miri engine removes the allocation. Pointers to dead allocations will cause a const eval error if they end up in the final constant.
Final values of constants and statics
If a constant's final value were of type String
, and the string is not empty, it would be very problematic to use such a constant:
const FOO: String = String::from("foo");
let x = FOO;
drop(x);
// how do we ensure that we don't run `deallocate`
// on the pointer to the unnamed static containing the bye sequence "foo"?
While there are a few options that could be considered, all of them are very hard to reason about and easy to get wrong. I'm listing them for completeness:
- just set the capacity to zero during const eval
- will prevent deallocation from doing anything
- seems like it would require crazy hacks in const eval which know about types with heap allocations inside
- Not sure how that would work for
Box
- use a custom allocator that just doesn't deallocate
- requires making every single datastructure generic over the allocator in use
- doesn't fit the "const eval that just works" mantra
- actually turn const eval heap allocations into real heap allocations on instantiation
- not zero cost
- use of a constant will trigger a heap allocation
We cannot ban types that contain heap allocations, because
struct Foo;
impl Drop for Foo {
fn drop(&mut self) {
println!("foo");
}
}
const FOO: Foo = Foo;
is perfectly legal stable Rust today. While we could try to come up with a scheme that forbids types that can contain allocations inside, this is impossible very hard to do.
There's a dynamic way to check whether dropping the value is problematic:
run
Drop::drop
on a copy of the final value (in const eval), if it tries to deallocate anything during that run, emit an error
Now this seems very dynamic in a way that means changing the code inside a const impl Drop
is a breaking change if it causes any deallocations where it did not before. This also means that it's a breaking change to add any allocations to code modifying or creating such values. So if SmallVec
(a type not heap allocating for N elements, but allocating for anything beyond that) changes the N
, that's a breaking change.
But the rule would give us the best of all worlds:
const A: String = String::new(); // Ok
const B: String = String::from("foo"); // Not OK
const C: &String = &String::from("foo"); // Ok
const D: &str = &String::from("foo"); // Ok
More alternatives? Ideas? Code snippets to talk about?
Current proposal/summary: #20 (comment)