Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracking issue for const fn pointers #63997

Open
gnzlbg opened this issue Aug 29, 2019 · 24 comments
Open

Tracking issue for const fn pointers #63997

gnzlbg opened this issue Aug 29, 2019 · 24 comments
Labels
A-const-fn Area: const fn foo(..) {..}. Pure functions which can be applied at compile time. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC S-tracking-needs-summary Status: It's hard to tell what's been done and what hasn't! Someone should do some investigation. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@gnzlbg
Copy link
Contributor

gnzlbg commented Aug 29, 2019

Sub-tracking issue for #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.

@jonas-schievink jonas-schievink added A-const-fn Area: const fn foo(..) {..}. Pure functions which can be applied at compile time. B-RFC-approved Blocker: Approved by a merged RFC but not yet implemented. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue. labels Aug 29, 2019
@gnzlbg
Copy link
Contributor Author

gnzlbg commented Aug 29, 2019

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:

  • const must be part of fn types (just like unsafe, the extern "ABI", etc.)
  • we should allow calling const fn types from const fn

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
Copy link
Member

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

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Aug 29, 2019

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 Centril removed B-RFC-approved Blocker: Approved by a merged RFC but not yet implemented. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Aug 29, 2019
@Centril
Copy link
Contributor

Centril commented Aug 29, 2019

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
Copy link
Contributor

oli-obk commented Aug 29, 2019

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
Copy link
Member

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
Copy link

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

@luser
Copy link
Contributor

luser commented Jan 9, 2020

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
Copy link
Contributor

oli-obk commented Jan 9, 2020

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
Copy link
Contributor

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
Copy link

filtsin commented Jul 21, 2020

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
Copy link

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
Copy link

filtsin commented Jul 21, 2020

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
Copy link

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
Copy link
Contributor

jplatte commented Jul 21, 2020

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
Copy link
Contributor

jplatte commented Jul 21, 2020

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
Copy link

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
Copy link
Contributor

beepster4096 commented Mar 12, 2021

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
Copy link

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). #83033

@RalfJung
Copy link
Member

RalfJung commented May 3, 2021

See my response at #83033 (comment) -- short summary: there is in fact a cast going on here; see the reference on "function item types" for more details.

@ketsuban
Copy link
Contributor

ketsuban commented Dec 4, 2021

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
Copy link
Contributor

oli-obk commented Dec 7, 2021

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.

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Mar 7, 2022
…r=wesleywiser

Stabilize const_fn_fn_ptr_basics, const_fn_trait_bound, and const_impl_trait

# Stabilization Report

This PR serves as a request for stabilization for three const evaluation features:

1. `const_fn_fn_ptr_basics`
2. `const_fn_trait_bound`
3. `const_impl_trait`

These are being stabilized together because they are relatively minor and related updates to existing functionality.

## `const_fn_fn_ptr_basics`

Allows creating, passing, and casting function pointers in a `const fn`.

The following is an example of what is now allowed:

```rust
const fn get_function() -> fn() {
    fn foo() {
        println!("Hello, World!");
    }

    foo
}
```

Casts between function pointer types are allowed, as well as transmuting from integers:

```rust
const fn get_function() -> fn() {
    unsafe {
        std::mem::transmute(0x1234usize)
    }
}
```

However, casting from a function pointer to an integer is not allowed:

```rust
const fn fn_to_usize(f: fn()) -> usize {
    f as usize  //~ pointers cannot be cast to integers during const eval
}
```

Calling function pointers is also not allowed.

```rust
const fn call_fn_ptr(f: fn()) {
    f() //~ function pointers are not allowed in const fn
}
```

### Test Coverage

The following tests include code that exercises this feature:

- `src/test/ui/consts/issue-37550.rs`
- `src/test/ui/consts/issue-46553.rs`
- `src/test/ui/consts/issue-56164.rs`
- `src/test/ui/consts/min_const_fn/allow_const_fn_ptr_run_pass.rs`
- `src/test/ui/consts/min_const_fn/cast_fn.rs`
- `src/test/ui/consts/min_const_fn/cmp_fn_pointers.rs`

## `const_fn_trait_bound`

Allows trait bounds in `const fn`. Additionally, this feature allows creating and passing `dyn Trait` objects.

Examples such as the following are allowed by this feature:

```rust
const fn do_thing<T: Foo>(_x: &T) {
    // ...
}
```

Previously only `Sized` was allowed as a trait bound.

There is no way to call methods from the trait because trait methods cannot currently be marked as const. Allowing trait bounds in const functions does allow the const function to use the trait's associated types and constants.

This feature also allowes `dyn Trait` types. These work equivalently to non-const code. Similar to other pointers in const code, the value of a `dyn Trait` pointer cannot be observed.

Note that due to rust-lang#90912, it was already possible to do the example above as follows:

```rust
const fn do_thing<T>(_x: &T) where (T,): Foo {
    // ...
}
```

### Test Coverage

The following tests include code that exercises `const_fn_trait_bound`:

- `src/test/ui/consts/const-fn.rs`
- `src/test/ui/consts/issue-88071.rs`
- `src/test/ui/consts/min_const_fn/min_const_fn.rs`
- `src/test/ui/consts/min_const_fn/min_const_fn_dyn.rs`
- `src/test/ui/nll/issue-55825-const-fn.rs`
- Many of the tests in `src/test/ui/rfc-2632-const-trait-impl/` also exercise this feature.

## `const_impl_trait`

Allows argument and return position `impl Trait` in a `const fn`, such as in the following example:

```rust
const fn do_thing(x: impl Foo) -> impl Foo {
    x
}
```

Similar to generic parameters and function pointers, this allows the creation of such opaque types, but not doing anything with them beyond accessing associated types and constants.

### Test Coverage

The following tests exercise this feature:

- `src/test/ui/type-alias-impl-trait/issue-53096.rs`
- `src/test/ui/type-alias-impl-trait/issue-53678-generator-and-const-fn.rs`

## Documentation

These features are documented along with the other const evaluation features in the Rust Reference at https://doc.rust-lang.org/stable/reference/const_eval.html.

There is a PR that updates this documentation to reflect the capabilities enabled by these features at rust-lang/reference#1166.

Tracking issues: rust-lang#57563, rust-lang#63997, rust-lang#93706
@joshtriplett joshtriplett added the S-tracking-needs-summary Status: It's hard to tell what's been done and what hasn't! Someone should do some investigation. label Jun 22, 2022
@est31
Copy link
Member

est31 commented Mar 16, 2023

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 38a0b81...03918ba 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
Copy link

onlycs commented Dec 29, 2023

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> {
...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-const-fn Area: const fn foo(..) {..}. Pure functions which can be applied at compile time. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC S-tracking-needs-summary Status: It's hard to tell what's been done and what hasn't! Someone should do some investigation. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests