rust-lang / rust

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

Tracking issue for const fn pointers #63997

Open gnzlbg opened 5 years ago

gnzlbg commented 5 years ago

Sub-tracking issue for https://github.com/rust-lang/rust/issues/57563.

This tracks const fn types and calling fn types in const fn.


From the RFC (https://github.com/oli-obk/rfcs/blob/const_generic_const_fn_bounds/text/0000-const-generic-const-fn-bounds.md#const-function-pointers):

const function pointers

const fn foo(f: fn() -> i32) -> i32 {
    f()
}

is illegal before and with this RFC. While we can change the language to allow this feature, two questions make themselves known:

  1. fn pointers in constants

    const F: fn() -> i32 = ...;

    is already legal in Rust today, even though the F doesn't need to be a const function.

  2. Opt out bounds might seem unintuitive?

    const fn foo(f: ?const fn() -> i32) -> i32 {
        // not allowed to call `f` here, because we can't guarantee that it points to a `const fn`
    }
    const fn foo(f: fn() -> i32) -> i32 {
        f()
    }

Alternatively one can prefix function pointers to const functions with const:

const fn foo(f: const fn() -> i32) -> i32 {
    f()
}
const fn bar(f: fn() -> i32) -> i32 {
    f() // ERROR
}

This opens up the curious situation of const function pointers in non-const functions:

fn foo(f: const fn() -> i32) -> i32 {
    f()
}

Which is useless except for ensuring some sense of "purity" of the function pointer ensuring that subsequent calls will only modify global state if passed in via arguments.

gnzlbg commented 5 years ago

I think that, at the very least, this should work:

const fn foo() {}
const FOO: const fn() = foo;
const fn bar() { FOO() }
const fn baz(x: const fn()) { x() }
const fn bazz() { baz(FOO) }

For this to work:

Currently, const fns already coerce to fns, so const fn types should too:

const fn foo() {}
let x: const fn() = foo;
let y: fn() = x; // OK: const fn => fn coercion

I don't see any problems with supporting this. The RFC mentions some issues, but I don't see anything against just supporting this restricted subset.

This subset would be super useful. For example, you could do:

struct Foo<T>(T);
trait Bar { const F: const fn(Self) -> Self; }

impl<T: Bar> Foo<T> {
    const fn new(x: T) -> Self { Foo(<T as Bar>::F(x)) }
}

const fn map_i32(x: i32) -> i32 { x * 2 }
impl Bar for i32 { const F: const fn(Self) -> Self = map_i32; } 
const fn map_u32(x: i32) -> i32 { x * 3 }
impl Bar for u32 { const F: const fn(Self) -> Self = map_u32; } 

which is a quite awesome work around for the lack of const trait methods, but much simpler since dynamic dispatch isn't an issue, as opposed to:

trait Bar { const fn map(self) -> Self; } 
impl Bar for i32 { ... }
impl Bar for u32 { ... }
// or const impl Bar for i32 { ... {

This is also a way to avoid having to use if/match etc. in const fns, since you can create a trait with a const, and just dispatch on it to achieve "conditional" control-flow at least at compile-time.

RalfJung commented 5 years ago

AFAIK const fn types are not even RFC'd, isn't it too early for a tracking issue?

gnzlbg commented 5 years ago

Don't know, @centril suggested that I open one.

I have no idea why const fn types aren't allowed. AFAICT, whether a function is const or not is part of its type, and the fact that const fn is rejected in a type is an implementation / original RFC oversight. If this isn't the case, what is the case?

EDIT: If I call a non-const fn from a const fn, that code fails to type check, so for that to happen const must be part of a fn type.

Centril commented 5 years ago

AFAIK const fn types are not even RFC'd, isn't it too early for a tracking issue?

Lotsa things aren't RFCed with respect to const fn. I want these issues for targeted discussion so it doesn't happen on the meta issue.

oli-obk commented 5 years ago

If I call a non-const fn from a const fn, that code fails to type check, so for that to happen const must be part of a fn type.

it is.

you can do

const fn f() {}
let x = f;
x();

inside a constant. But this information is lost when casting to a function pointer. Function pointers just don't have the concept of of constness.

RalfJung commented 5 years ago

Figuring out constness in function pointers or dyn traits is a tricky questions, with a lot of prior discussion in the RFC and the pre-RFC.

tema3210 commented 5 years ago

whats about? pub trait Reflector { fn Reflect(&mut self)-> (const fn(Cow<str>)->Option<Descriptor>); }

luser commented 4 years ago

I was fiddling with something while reading some of the discussion around adding a lazy_static equivalent to std and found that this check forbids even storing a fn pointer in a value returned from a const fn which seems unnecessarily restrictive given that storing them in const already works. The standard lazy types RFC winds up defining Lazy like:

pub struct Lazy<T, F = fn() -> T> { ... }

Here's a simple (but not very useful) example that hits this.

Adding another type parameter for the function makes it work on stable but it feels unnecessary.

Could this specific case be allowed without stabilizing the entire ball of wax here? (Specifically: referencing and storing fn pointers in const fn but not calling them.)

oli-obk commented 4 years ago

Could this specific case be allowed without stabilizing the entire ball of wax here? (Specifically: referencing and storing fn pointers in const fn but not calling them.)

The reason we can't do this is that this would mean we'd lock ourselves into the syntax that fn() means a not callable function pointer (which I do realize constants already do) instead of unifying the syntax with trait objects and trait bounds as shown in the main post of this issue

jonas-schievink commented 4 years ago

Just in case other people run into this being unstable: It's still possible to use function pointers in const fn as long as they're wrapped in some other type (eg. a #[repr(transparent)] newtype or an Option<fn()>):

#[repr(transparent)]
struct Wrap<T>(T);

extern "C" fn my_fn() {}

const FN: Wrap<extern "C" fn()> = Wrap(my_fn);

struct Struct {
    fnptr: Wrap<extern "C" fn()>,
}

const fn still_const() -> Struct {
    Struct {
        fnptr: FN,
    }
}
filtsin commented 4 years ago

If const is a qualifier of function like unsafe or extern we have next issue:

const fn foo() { }
fn bar() { }

fn main() {
    let x = if true { foo } else { bar };
}

It compiles now but not compiles with these changes because if and else have incompatible types. So it breaks an old code.

tema3210 commented 4 years ago

If const is a qualifier of function like unsafe or extern we have next issue:

const fn foo() { }
fn bar() { }

fn main() {
    let x = if true { foo } else { bar };
}

It compiles now but not compiles with these changes because if and else have incompatible types. So it breaks an old code.

Const fn's are still fns, the type won't be charged. In fact const qualifier only allows const fn appear in const contexes, and be evaluated at compile time. You can think of this like implicit casting.

filtsin commented 4 years ago
unsafe fn foo() { }
fn bar() { }

fn main() {
    let x = if true { foo } else { bar };
}

This code does not compile for the same reason. Unsafe fn's are still fns too, why we haven't implicit casting in this situation?

tema3210 commented 4 years ago
unsafe fn foo() { }
fn bar() { }

fn main() {
    let x = if true { foo } else { bar };
}

This code does not compile for the same reason. Unsafe fn's are still fns too, why we haven't implicit casting in this situation?

This leads to possible unsafety in our code, and all which comes with it. const fn casting on otherside don't brings any unsafety, so is allowed, const fn must not have any side effects only, it is compatible with fn contract.

fn bar() {}

const fn foo(){}

const fn foo_bar(){
    if true { foo() } else { bar() };
}

This must not compile, because bar is not const and therefore can't be evaluated at compile time. Btw, it raises(?) "can't call non const fn inside of const one", and can be considered incorrect downcasting. (set of valid const fns is smaller than set of fns at all)

jplatte commented 4 years ago

fn main() { let x = if true { foo } else { bar }; }



This code does not compile for the same reason. Unsafe fn's are still fns too, why we haven't implicit casting in this situation?

This leads to possible unsafety in our code, and all which comes with it. const fn casting on otherside don't brings any unsafety, so is allowed, const fn must not have any side effects only, it is compatible with fn contract.

I think you're missing the point. With implicit coercions, the type of x would be unsafe fn(), not fn(). There's nothing about that which leads to possible unsafety. Generally, const fn() can be coerced to fn() and fn() can be coerced to unsafe fn(). It just doesn't happen automatically, which is why changing const fn foo() to coerce into const fn() rather than fn() implicitly is a breaking change.

fn bar() {}

const fn foo(){}

const fn foo_bar(){
    if true { foo() } else { bar() };
}

This must not compile, because bar is not const and therefore can't be evaluated at compile time. Btw, it raises(?) "can't call non const fn inside of const one", and can be considered incorrect downcasting. (set of valid const fns is smaller than set of fns at all)

Of course this must not compile, but I don't think that's related to what @filtsin was talking about.

jplatte commented 4 years ago

Personally I would love to see more implicit coercions for function pointers. Not sure how feasible that is though. I've previously wanted trait implementations for function pointer types to be considered when passing a function (which has a unique type) to a higher-order generic function. I posted about it on internals, but it didn't receive much attention.

tema3210 commented 4 years ago

Generally, const fn() can be coerced to fn() and fn() can be coerced to unsafe fn(). It just doesn't happen automatically, which is why changing const fn foo() to coerce into const fn() rather than fn() implicitly is a breaking change.

But not in oposite direction - thats what i wanted to say.

beepster4096 commented 3 years ago
impl<T> Cell<T> {
    pub fn with<U>(&self, func: const fn(&mut T) -> U) -> U;
}

Wouldn't const fn pointers make this sound? A const fn can't access a static or a thread-local, which would make this unsound.

Edit: a cell containing a reference to itself makes this unsound

HindrikStegenga commented 3 years ago

It seems assignment is currently broken, complaining about casts, even though no such casts are actually performed. (A const function simply assigning a function ptr basically). https://github.com/rust-lang/rust/issues/83033

RalfJung commented 3 years ago

See my response at https://github.com/rust-lang/rust/issues/83033#issuecomment-831089437 -- short summary: there is in fact a cast going on here; see the reference on "function item types" for more details.

ketsuban commented 3 years ago

Just in case other people run into this being unstable: It's still possible to use function pointers in const fn as long as they're wrapped in some other type (eg. a #[repr(transparent)] newtype or an Option<fn()>):

I've found a case where this isn't true. (playground link)

struct Handlers([Option<fn()>; _]);

impl Handlers {
    const fn new() -> Self {
        Self([None; _])
    }
}
oli-obk commented 2 years ago

Yea, just like with dyn Trait or generic trait bounds, there are workarounds to our checks and I'm convinced now we should just allow all of these like we do in const items. You can't call them, but you can pass them around.

est31 commented 1 year ago

For const closures a lot has happened in 2022.

This now works since rustc 1.61.0-nightly (03918badd 2022-03-07) (commit range is 38a0b81b1...03918badd but I can't narrow it down to a single PR... maybe a side effect of #93827 ???):

#![feature(const_trait_impl)]

const fn foo<T: ~const Fn() -> i32>(f: &T) -> i32 {
    f()
}

And since rustc 1.68.0-nightly (9c07efe84 2022-12-16) (I suppose the PR was #105725) you can even use impl Trait syntax:

#![feature(const_trait_impl)]

const fn foo(f: &impl ~const Fn() -> i32) -> i32 {
    f()
}

For const fn pointers, nothing much has happened though. This still errors:

const fn foo(f: ~const fn() -> i32) -> i32 {
    f()
}

gives

``` error: expected identifier, found keyword `fn` --> src/lib.rs:2:24 | 2 | const fn foo(f: ~const fn() -> i32) -> i32 { | ^^ | help: use `Fn` to refer to the trait | 2 | const fn foo(f: ~const Fn() -> i32) -> i32 { | ~~ error: `~const` is not allowed here --> src/lib.rs:2:17 | 2 | const fn foo(f: ~const fn() -> i32) -> i32 { | ^^^^^^^^^^^^^^^^^^ | = note: trait objects cannot have `~const` trait bounds error[E0782]: trait objects must include the `dyn` keyword --> src/lib.rs:2:17 | 2 | const fn foo(f: ~const fn() -> i32) -> i32 { | ^^^^^^^^^^^^^^^^^^ | help: add `dyn` keyword before this trait | 2 | const fn foo(f: dyn ~const fn() -> i32) -> i32 { | +++ For more information about this error, try `rustc --explain E0782`. error: could not compile `playground` (lib) due to 3 previous errors ```

Edit: note that while what I said is great progress, the two aren't well comparable, as the Fn trait support is for monomorphized generics cases, as in those where we know the type at const eval time and can derive the function from the type. dyn Fn trait support is still not existent. This gives a bunch of errors:

#![feature(const_trait_impl)]
const fn foo(f: &dyn ~const Fn() -> i32) -> i32 {
    f()
}
onlycs commented 11 months ago

Seems to have been patched. You need #[const_trait] attribute to use ~const, which the Fn trait doesn't have. rust-std says that it has "effects"

...
#[must_use = "closures are lazy and do nothing unless called"]
// FIXME(effects) #[const_trait]
pub trait Fn<Args: Tuple>: FnMut<Args> {
...