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

const fn taking function pointers and generic args with trait bounds #53972

Closed
oli-obk opened this issue Sep 5, 2018 · 10 comments
Closed

const fn taking function pointers and generic args with trait bounds #53972

oli-obk opened this issue Sep 5, 2018 · 10 comments
Labels
A-const-eval Area: Constant evaluation, covers all const contexts (static, const fn, ...) C-feature-request Category: A feature request, i.e: not implemented / a PR. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@oli-obk
Copy link
Contributor

oli-obk commented Sep 5, 2018

continued from #53555 (comment) (cc @SimonSapin @Centril )

Isn’t it already too late to make const the default, and non-constness require opt-in?

not really. we have restricted min_const_fn to the point where all futures are possible.

Resolving inconsistencies

I'm proposing that we stabilize function pointers in const fns by not allowing calling them until a future syntax enables us to call them. Below a possible syntax is sketched to show that such a scheme is possible

struct Foo(fn());
const fn foo(f: Foo) {
    // can't call `(f.0)()` here
}
const fn foo2(f: const Foo) {
    // can't call `(f.0)()` here, see also `bar1`
}
const fn foo3(f: fn()) {
    // can't call `f()` here
}
const fn foo4(f: const fn()) {
    f() // legal
}
fn not_const1(f: const fn()) {} // not legal, const modifier in non-const fn
fn not_const2() {
    fn nop() {}
    foo3(nop); // legal even though `nop` is no `const fn` because we're not in a const environment
}
struct Bar(const fn());
const fn bar(f: Bar) {
    (f.0)() // not allowed
}
const fn bar1(f: const Bar) {
    (f.0)() // legal
}
const impl Bar {
    fn bar(&self) {
        (self.0)() // legal
    }
}

Trait objects and generic trait bounds

Similar to the above I propose that one can specify arbitrary trait bounds and trait objects but not call any methods on them. Future syntax can opt into const trait objects and const trait bounds, enforcing these if the const fn is called inside a constant environment (const/static initializer, array length, enum discriminant)

@oli-obk oli-obk added A-const-fn C-feature-request Category: A feature request, i.e: not implemented / a PR. labels Sep 5, 2018
@SimonSapin
Copy link
Contributor

Regarding making const the default, I meant in function definitions. Is anyone seriously proposing that the syntax for various kinds of function pointer types should use different keywords than the syntax for function definitions?


const fn foo2(f: const Foo) {
const fn bar1(f: const Bar) {

Wait, const is not just a modifier for fn() types? How does that work?

@oli-obk
Copy link
Contributor Author

oli-obk commented Sep 5, 2018

const could be a modifier for any type, making the modified type usable in constants/const fn beyond "just assigning values of it to variables and fields". Otherwise I don't see how we could ever call Bar's field

Is anyone seriously proposing that the syntax for various kinds of function pointer types should use different keywords than the syntax for function definitions?

I don't understand this statement. Can you elaborate?

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the PR/issue. label Sep 5, 2018
@SimonSapin
Copy link
Contributor

I was initially responding to

#53555 (comment)

Particularly if we expect const fn to be extremely common (even more common than fn is eventually if everything goes right...), which I'm hoping it will.

… which seemed to suggest that somehow a type fn() could be assumed to be const, and we’d use another keyword for the reverse like notconst fn().

@Centril
Copy link
Contributor

Centril commented Sep 5, 2018

(I'm fine with this being discussed here, but whatever else, changes proposed here will absolutely require one or several RFCs... and just to be super clear, I'm wary of doing anything quickly here...)

… which seemed to suggest that somehow a type fn() could be assumed to be const, and we’d use another keyword for the reverse like notconst fn().

The idea with such a design (I'm not for such a design atm...) is that you wouldn't have notconst fn() so there are certain things you wouldn't be able to express.

const could be a modifier for any type, making the modified type usable in constants/const fn beyond "just assigning values of it to variables and fields".

This has large implications for const generics; me and @varkor discussed such ideas on Discord wrt. const-sigma types. In particular, this implies to me that you would write fn foo<N: const usize> rather than fn foo<const N: usize>.

With respect to:

struct Bar(const fn());
const fn bar(f: Bar) {
    (f.0)() // not allowed
}

I find that deeply surprising; by construction we know that (x : Bar).0 is const fn, so it should be safe to call it within bar. Not allowing this would break the whole "modifier on type/trait" idea.

@oli-obk
Copy link
Contributor Author

oli-obk commented Sep 5, 2018

I find that deeply surprising; by construction we know that (x : Bar).0 is const fn, so it should be safe to call it within bar. Not allowing this would break the whole "modifier on type/trait" idea.

In that case you couldn't have

const fn new(f: fn()) -> Bar {
    Bar(f)
}

without additionally having some sort of notconst. We can have both worlds by requiring const on the type even if the field is already const, because you're talking about what you want to do with the value.

@RalfJung
Copy link
Member

RalfJung commented Sep 5, 2018

I thought we have rust-lang/const-eval#1 for this discussion...?

@Centril
Copy link
Contributor

Centril commented Sep 5, 2018

@oli-obk

const fn new(f: fn()) -> Bar {
    Bar(f)
}

You have defined Bar as struct Bar(const fn()); -- so Bar(f) should imo absolutely be illegal.

@oli-obk
Copy link
Contributor Author

oli-obk commented Sep 6, 2018

We treat const fn() pointers in const fn arguments as if it were fn() if the const fn is called at runtime. I don't see why this would not extend to all other uses of const fn pointers.

@RalfJung I'll summarize the findings of this issue in rust-lang/const-eval#1 at the end of the week and then close this issue. I totally forgot about that issue ^^

@oli-obk
Copy link
Contributor Author

oli-obk commented Sep 6, 2018

Ok. So there are various ifs here. Summarizing the state by using function pointers as a placeholder scapegoat. The same logic applied to function pointers can be applied to dyn Trait and impl Trait arguments.

fn foo() { if random() { println!("foo") } }
const F: fn() = foo;

is legal in stable Rust today. This means that

const G: () = F();

cannot be evaluated, because from the signature of F we do not know whether F's value is a function pointer to a const fn.

It seems consistent to also say that in a function

const fn bar(f: fn()) { ... }

we cannot call f, because we do not know whether it points to a const fn

Thus, I argue, we need new syntax to specify that a function pointer is callable at compile-time.

Let's for a moment assume that we can use const fn() (syntax can be bikeshed) as a type to specifiy that the variable of such a type points to a const fn and we can thus call it in a const fn. Thus

const fn bar(f: const fn()) { f() }
const fn bar2(f: fn()) -> fn() { f }
const fn bar3(f: const fn()) -> fn() { f(); f }
const fn bar4(f: const fn()) -> const fn() { f(); f }

would all be legal Rust. In order to reduce code duplication and not require non-const duplicates of bar3 and bar4 callable without a const fn argument, I presume we would want to allow calling bar3 and bar4 outside of const environments by passing arbitrary function pointers.

In hypothetical "const effect" syntax that could be formulated as

const<X> fn bar(f: const<X> fn()) { f() }
const<X> fn bar2(f: fn()) -> fn() { f }
const<X> fn bar3(f: const<X> fn()) -> fn() { f(); f }
const<X> fn bar4(f: const<X> fn()) -> const<X> fn() { f(); f }

where the use of const<X> in an output means that its value is derived from an input with const<X> and thus const iff the input was const.

In servo common uses of function pointers in const fns are for initializing datastructures. So

struct Foo {
    f: fn(),
    g: fn(),
}
const fn foo_new(f: fn(), g: fn()) -> Foo {
    Foo { f, g }
}

would naturally exist in this system.

Such types have a downside though, one cannot in hindsight attach const to the function pointers when using Foo as an argument to a const fn:

const fn exec_f(foo: Foo) {
    (foo.f)() // ERROR, do not know whether `foo.f` is a const fn
}

There are a few possible solutions to this:

// force `g` to be `const` even though we don't need that
const fn exec_f(foo: const Foo);

// effect system
const<F> struct Foo {
    f: const<F> fn(),
    g: fn(), // if the library author forgot to add an effect to `g`
    // you can't write a const fn to call it even if you know about it
}
const<F> exec_f(foo: Foo<F>);

// where bounds which only matter at compile-time
const fn exec_f(foo: Foo) where foo.f: const fn();

The third solution seems to be the most powerful solution, but there's no way to reason about the return value, so we'd also need some more magic like where return: const fn() if we'd want to propagate the constness to the return type. At which point we're back at effect systems since we might want where foo.f: const fn() if foo.f: const fn() or other conditional bounds.

It's not all that relevant to stabilizing the use of function pointers in the "cannot call function pointers in const fn"-scheme, since that scheme is the only one consistent with what we're doing in constants right now. Any way to call const fns through functions pointers will need extra syntax, so we can "just" stabilize function pointers right now.

Trait objects follow a similar reasoning

trait Foo {
    fn bar(&self);
}
impl Foo for () {
    fn bar(&self) {}
}
const D: &Foo = &();

is legal Rust today, but there's no way to ensure that

const E: () = D.bar();

would be const evaluable without changing E and Foo to have new syntax opting into the constness

Generics are in a similar situation.

trait Foo {
   fn new() -> Self;
}
const fn foo<T: Foo>() -> T {
    T::new()
}
const fn foo2<T: Foo>() {
}

foo needs some way to specifiy that new is a const fn for the specific T chosen (or for all T). foo2 doesn't need any <T as Foo> method to be const fn. We need a way to ensure that callers to foo and foo2 know the constraints on both functions without randomly breaking semver compatibility by making the constraints inferred by the function body.

While we could default to "must be const fn callable", that would break the very common use case of things like const fn new<T: Debug>(t: T) -> StructWithPrivFields { StructWithPrivFields { t } }. While it can be argued that the T bound is unnecessary here, this is very common code (especially with the bounds on the impl block).

@oli-obk
Copy link
Contributor Author

oli-obk commented Sep 6, 2018

rereading @RalfJung 's post rust-lang/const-eval#1 (comment) it seems we have come to the same conclusion

@oli-obk oli-obk closed this as completed Sep 6, 2018
@RalfJung RalfJung added the A-const-eval Area: Constant evaluation, covers all const contexts (static, const fn, ...) label Dec 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-const-eval Area: Constant evaluation, covers all const contexts (static, const fn, ...) C-feature-request Category: A feature request, i.e: not implemented / a PR. 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

4 participants