rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.01k stars 12.54k forks source link

const fn tracking issue (RFC 911) #24111

Closed nikomatsakis closed 5 years ago

nikomatsakis commented 9 years ago

https://github.com/rust-lang/rust/issues/57563 | new meta tracking issue

Old content

Tracking issue for rust-lang/rfcs#911.

This issue has been closed in favor of more targeted issues:

Things to be done before stabilizing:

CTFE = https://en.wikipedia.org/wiki/Compile_time_function_execution

petrochenkov commented 7 years ago

stabilize uses of const fn.

This also stabilizes some functions in the standard library as being const, the library team should do some audit at least.

durka commented 7 years ago

I have submitted a PR https://github.com/rust-lang/rust/issues/43017 to stabilize invocations, along with a list of functions to be audited per @petrochenkov.

ghost commented 7 years ago

I have a question/comment about how this could be used in certain trait/impl situations. Hypothetically, let's say we have a math library with a Zero trait:

pub trait Zero {
    fn zero () -> Self;
}

This trait does not require the zero method to be const, as this would prevent it from being impled by some BigInt type backed by a Vec. But for machine scalars and other simple types, it would be far more practical if the method were const.

impl Zero for i32 {
    const fn zero () -> i32 { 0 } // const
}

impl Zero for BigInt {
    fn zero () -> BigInt { ... } // not const
}

The trait does not require that the method be const, but it should still be allowed, as const is adding a restriction to the implementation and not ignoring one. This prevents having a normal version and a const version of the same function for some types. What I'm wondering is has this already been addressed?

jethrogb commented 7 years ago

Why would you want different implementations of the trait to behave differently? You can't use that in a generic context. You can just make a local impl on the scalar with a const fn.

eddyb commented 7 years ago

@Daggerbot That is the only way I see forward for const fn in traits - having the trait require that all impls are const fn is far less common than having effectively "const impls".

@jethrogb You could, although it requires the constness be a property of the impl. What I have in mind is that a generic const fn with an, e.g. T: Zero bound, will require the impl of Zero for the Ts it is called with to contain only const fn methods, when the call comes from a constant context itself (e.g. another const fn).

It's not perfect but no superior alternative has been put forward - IMO the closest to that would be "allow any calls and error deep from the call stack if anything not possible at compile-time is attempted", which isn't as bad as it may seem at a first impression - most of the concern over it has to do with backwards compatibility, i.e. marking a function const fn ensures that the fact is recorded and performing operations not valid at compile-time would require making it not const fn.

Kixunil commented 7 years ago

Wouldn't this solve the issue?

pub trait Zero {
    fn zero() -> Self;
}

pub trait ConstZero: Zero {
    const fn zero() -> Self;
}

impl<T: ConstZero> Zero for T {
    fn zero() -> Self {
        <Self as ConstZero>::zero()
    }
}

The boilerplate could be decreased with macros.

ghost commented 7 years ago

Apart from the minor inconvenience of having two separate traits (Zero and ConstZero) which do almost exactly the same thing, I see one potential problem when using a blanket implementation:

// Blanket impl
impl<T: ConstZero> Zero for T {
    fn zero () -> Self { T::const_zero() }
}

pub struct Vector2<T> {
    pub x: T,
    pub y: T,
}

impl<T: ConstZero> ConstZero for Vector2<T> {
    const fn const_zero () -> Vector2<T> {
        Vector2 { x: T::const_zero(), y: T::const_zero() }
    }
}

// Error: This now conflicts with the blanket impl above because Vector2<T> implements ConstZero and therefore Zero.
impl<T: Zero> Zero for Vector2<T> {
    fn zero () -> Vector2<T> {
        Vector2 { x: T::zero(), y: T::zero() }
    }
}

The error would go away if we removed the blanket impl. All in all, this is probably the easiest to implement in a compiler as it adds the least complexity to the language.

But if we could add const to an implemented method where it is not required, we can avoid this duplication, although still not perfectly:

impl<T: Zero> Zero for Vector2<T> {
    const fn zero () -> Vector2<T> {
        Vector2 { x: T::zero(), y: T::zero() }
    }
}

IIRC, C++ allows something like this when working with constexpr. The downside here is that this const would only be applicable if <T as Zero>::zero is also const. Should this be an error, or should the compiler ignore this const when it is not applicable (like C++)?

Neither of these examples tackle the problem perfectly, but I can't really think of a better way.

Edit: @andersk's suggestion would make the first example possible without errors. This would probably be the best/simplest solution as far as compiler implementation goes.

andersk commented 7 years ago

@Daggerbot This sounds like a use case for the “lattice” rule proposed near the end of RFC 1210 (specialization). If you write

impl<T: ConstZero> Zero for T {…}  // 1
impl<T: ConstZero> ConstZero for Vector2<T> {…}  // 2
impl<T: Zero> Zero for Vector2<T> {…}  // 3
impl<T: ConstZero> Zero for Vector2<T> {…}  // 4

then although 1 overlaps with 3, their intersection is covered precisely by 4, so it would be allowed under the lattice rule.

See also http://smallcultfollowing.com/babysteps/blog/2016/09/24/intersection-impls/.

eddyb commented 7 years ago

That is an incredibly complex system, which we want to avoid.

Kixunil commented 7 years ago

Yeah, lattice rule would be needed.

@eddyb what do you consider complex?

eddyb commented 7 years ago

@Kixunil Duplicating almost every single trait in the standard library, instead of "simply" marking some impls as const fn.

oli-obk commented 7 years ago

We're getting off track here. Currently the issue is about stabilizing uses of const fn. Allowing const fn trait methods or const impl Trait for Foo are orthogonal to each other and to the accepted RFCs.

eddyb commented 7 years ago

@oli-obk This is not the new RFC but the tracking issue for const fn.

oli-obk commented 7 years ago

I just noticed, and edited my comment.

Kixunil commented 7 years ago

@eddyb yeah, but it's simpler for compiler (minus specialization, but we will probably want specialization anyway) and allows people to bound by ConstTrait too.

Anyway, I'm not opposed to marking impls as const. I'm also imagining compiler auto-generating ConstTrait: Trait.

eddyb commented 7 years ago

@Kixunil It's not much simpler, especially if you can do it with specialization. The compiler wouldn't have to auto-generate anything like ConstTrait: Trait, nor does the trait system need to know about any of this, one just needs to recurse through the implementations (either a concrete impl or a where bound) that the trait system provides and check them.

Kixunil commented 7 years ago

I'm wondering if const fns should disallow accesses to UnsafeCell. It's probably necessary to allow truly const behavior:

const fn dont_change_anything(&self) -> bool {
    let old = self.cell.get();
    self.cell.set(!old);
    old
}

So far I've seen that set is not const. The question is whether this will stay forever. In other words: Can unsafe code rely that when running the same const function on immutable data will always return the same result today and in every future release of the language/library?

SimonSapin commented 7 years ago

const fn doesn’t mean immutable, it means can be called at compile-time.

Kixunil commented 7 years ago

I see. I'd much appreciate if I could somehow guarantee that a function always returns same thing when called multiple times without using unsafe traits, if it's possible somehow.

jethrogb commented 7 years ago

@Kixunil you want https://github.com/rust-lang/rfcs/issues/1631

Kixunil commented 7 years ago

@jethrogb Thanks for the link!

nvzqz commented 7 years ago

I noticed that mem::size_of is implemented as a const fn on nightly. Would this be possible for mem::transmute and others? Rust intrinsics operate internal to the compiler, and I'm not familiar enough to make the proper changes to allow this. Otherwise, I'd be happy to implement it.

oli-obk commented 7 years ago

Unfortunately operating on values is a little harder than just magically creating some. transmute requires miri. A fist step towards getting miri into the compiler is already underway: #43628

durka commented 6 years ago

So! Any interest in const-stabilizing *Cell::new, mem::{size,align}_of, ptr::null{,_mut}, Atomic*::new, Once::new and {integer}::{min,max}_value? Shall we have FCPs in here or create individual tracking issues?

SimonSapin commented 6 years ago

Yes.

SimonSapin commented 6 years ago

I’m not part of any team that has decision power on this, but my personal opinion is that all of these except mem::{size,align}_of are trivial enough that they could be stabilized now without going through the motions of a rubber-stamp FCP.

As a user I would like to use mem::{size,align}_of in const expressions as soon as possible, but I’ve read @nikomatsakis express concerns about them being insta-const-stable when they were made const fns. I don’t know if there are specific concerns or just general caution, but IIRC this is why per-function feature gates were added. I imagine the concerns for these two would be similar enough that they could share a FCP. I don’t know if @rustbot can handle separate FCPs in the same GitHub thread, so it’s probably better to open separate issues.

aturon commented 6 years ago

@durka can you open a single tracking issue for stabilizing the constness of all of those functions? I'll propose FCP once it's up.

SimonSapin commented 6 years ago

@aturon https://github.com/rust-lang/rust/issues/46038

mzabaluev commented 6 years ago

To follow a lead in a discussion about const fns on alloc::Layout: Can panic be allowed in a const fn and treated as a compilation error? This is similar to what is done now with constant arithmetic expressions, isn't it?

oli-obk commented 6 years ago

Yes that is a super trivial feature once miri is merged

remexre commented 6 years ago

Is this the right place to request additional std functions becoming const? If so, Duration::{new,from_secs,from_millis} should all be safe to make const.

SimonSapin commented 6 years ago

@remexre The easiest way to make it happen is probably to make a PR and ask for libs team review there.

remexre commented 6 years ago

PR'd as https://github.com/rust-lang/rust/pull/47300. I also added const to the unstable constructors while I was at it.

sfleischman105 commented 6 years ago

Any thoughts on allowing further std functions to be declared const? Specifically, mem::uninitialized and mem::zeroed? I believe both of these are suitable candidates for additional const functions. The only drawback I can think of is the same drawback of mem::uninitialized, where the creation of structs implementing Drop are created and written over without a ptr::write.

I can attach a PR as well if this sounds suitable.

durka commented 6 years ago

What is the motivation for that? It seems like a useless footgun to allow making invalid bit patterns that then can't be overwritten (because they're in a const), but maybe I'm overlooking the obvious.

sfleischman105 commented 6 years ago

mem::uninitialized is absolutely a footgun, one that shoots through your hands as well if aimed improperly. Seriously, I cannot overstate how incredibly dangerous the use of this function can be, despite its marking as unsafe.

The motivation behind declaring these additional functions const stems from the nature of these functions, as calling mem::uninitialized<Vec<u32>> will return the same result every time, with no side-effects. Obviously if left uninitialized, this is a terrible thing to do. Hence, the unsafe is still present.

But for a use-case, consider a global timer, one that tracks the start of some function. It's internal state will be determined at a later time, but we need a way to present it as a static global struct created on execution.


use std::time::Instant;

pub struct GlobalTimer {
    time: UnsafeCell<Instant>
}

impl TimeManager {
    pub const fn init() -> TimeManager {
        TimeManager {
            time: UnsafeCell::new(Instant::now())
        }
    }
}

This code doesn't compile, due to Instant::now() not being a const function. Replacing Instant::now() with mem::uninitialized::<Instant>()) would fix this problem if mem::uninitialized was a const fn. Ideally, the developer will initialize this structure once the program starts execution. And while this code is considered un-idiomatic rust (global state is generally very bad), this is just one of many cases where global static structures are useful.

I think this post gives a good foundation for the future of Rust code being run at compile-time. Global, compile-time static structures are a feature with some important use-cases (OS's also come to mind) that rust is currently missing. Small steps can be made towards this goal by think slowly adding const to library functions deemed suitable, such as mem::uninitialized and mem::zeroed, despite their unsafe markings.

Edit: Forgot the const in function signature of TimeManager::init()

durka commented 6 years ago

Hmm, that code does compile so I am still missing the exact motivation here... if you could write code such as

const fn foo() -> Whatever {
    unsafe { 
        let mut it = mem::uninitialized();
        init_whatever(&mut it);
        it
    }
}

But const fns are currently so restricted that you can't even write that...

I appreciate the theoretical justification, but const is not the same as pure and I don't think we should do anything to encourage the use of these functions if it's not necessary for some compelling use case.

oli-obk commented 6 years ago

I think there are much lower hanging fruit that could be stabilized first. Without miri, the uninitialized and zeroed intrinsics make little sense anyway. I would like to see them someday though. We could even initially stabilize them and require that all constants must produce an initialized result, even if intermediate computations can be uninitialized.

That said, with unions and unsafe code you can emulate uninitialized or zeroed anyway, so there's not much point in keeping them non-const

sfleischman105 commented 6 years ago

With the help of unions, the previous code now compiles. It's absolutely terrifying 😅.

All good points as well. These intrinsic functions are pretty low on the use-case list, but they are still suitable candidates for eventual const-ness.

durka commented 6 years ago

That is terrifyingly amazing.

So... why exactly are you advocating for constifying mem::uninitialized, as opposed to, say, Instant::now? :)

The need to have constant initializers for structs with non-constant interiors is real (see: Mutex). But I don't think making this malarkey easier is the right way to get that!

On Thu, Jan 25, 2018 at 2:21 AM, Stephen Fleischman < notifications@github.com> wrote:

With the help of unions, the previous code now compiles https://play.rust-lang.org/?gist=be075cf12f63dee3b2e2b65a12a3c854&version=nightly. It's absolutely terrifying 😅.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/rust-lang/rust/issues/24111#issuecomment-360382201, or mute the thread https://github.com/notifications/unsubscribe-auth/AAC3n-HyWD6MUEbfHkUUXonh9ORGPSRoks5tOCtegaJpZM4D66IA .

oli-obk commented 6 years ago

Instant::now cannot be const. What would that function return? The time of compilation?

jonas-schievink commented 6 years ago

Can someone summarize what needs to be done for stabilizing this? What decision needs to be reached? Whether to stabilize this at all?

Integration with patterns (e.g. https://gist.github.com/d0ff1de8b6fc15ef1bb6)

I've already commented on the gist, but given that const fn currently cannot be matched against in a pattern, this shouldn't block stabilization, right? We could always allow it afterwards if it makes sense.

tailhook commented 6 years ago

Instant::now cannot be const. What would that function return? The time of compilation?

But there might be Instant::zero() or Instant::min_value() which is const.

oli-obk commented 6 years ago

Can someone summarize what needs to be done for stabilizing this? What decision needs to be reached? Whether to stabilize this at all?

I think the only open question is whether our const fn checks are strict enough to not accidentally allow/stabilize something that we don't want inside const fn.

SoniEx2 commented 6 years ago

Can we do integration with patterns through rust-lang/rfcs#2272 ? Patterns are already painful as they currently are, let's not make them more painful.

sgrif commented 6 years ago

I think the only open question is whether our const fn checks are strict enough to not accidentally allow/stabilize something that we don't want inside const fn.

Correct me if I'm wrong, but aren't those checks identical to the checks to what is currently allowed in the body of a const rvalue? I was under the impression that const FOO: Type = { body }; and const FOO: Type = foo(); const fn foo() -> Type { body } are identical in what they allow for any arbitrary body

eddyb commented 6 years ago

@sgrif I think the concern is around arguments, which const fn have but const don't. Also, it's not clear that in the long term we want to keep the const fn system of today.

SoniEx2 commented 6 years ago

Are you suggesting const generics (both ways) instead? (e.g. <const T> + const C<T> aka const C<const T>?)

Badel2 commented 6 years ago

I would really like to have a try_const! macro which will try to evaluate any expression at compile time, and panic if not possible. This macro will be able to call non-const fns (using miri?), so we don't have to wait until every function in std has been marked const fn. However as the name implies, it can fail at any time, so if a function is updated and now can't be const, it will stop compiling.

leoschwarz commented 6 years ago

@Badel2 I understand why you'd want such a feature, but I suspect widespread use of it would be really bad for the crates ecosystem. Because this way your crate might end up depending on a function in another crate being compile time evaluable, and then the crate author changes something not affecting the function signature but preventing the function from being compile time evaluable.

If the function was marked const fn in the first place, then the crate author would have spotted the issue directly when trying to compile the crate and you can rely on the anotation.