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

Calling methods on generic parameters of const fns #2632

Open
wants to merge 17 commits into
base: master
from

Conversation

Projects
None yet
@oli-obk
Copy link
Contributor

oli-obk commented Feb 5, 2019

TLDR: Allow

const fn add<T: Add>(a: T, b: T) -> T::Output {
    a + b
}

and

pub struct Foo<T: Trait>(T);
impl<T: ?const Trait> Foo<T> {
    fn new(t: T) -> Self {
        // not calling methods on `t`, so we opt out of requiring
        // `<T as Trait>` to have const methods via `?const`
        Self(t)
    }
}

cc @Centril @varkor @RalfJung @eddyb

This RFC has gone through an extensive pre-RFC phase in rust-rfcs/const-eval#8

Rendered

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Feb 5, 2019

One alternative syntax design that is not mentioned here is the question of const impl versus impl const.

There are strong arguments that const impl is the more consistent syntax, as it is consistent both with the existing practice of prefixing modifiers to impl (default impl, unsafe impl, etc.) and prefixing const to keywords to indicate const variants (e.g. const fn [and the hypothetical const trait]).

Conversely, as far as I'm aware, impl const is not consistent with any existing syntax in the language or this RFC.


(I don't like to start the discussion with syntax bikeshedding, but as mentioned in the original post, this RFC has already ungone significant design discussion and I'm satisfied it's close to the optimal conservative design.)

@rpjohnst

This comment has been minimized.

Copy link

rpjohnst commented Feb 5, 2019

How do we want -> impl Trait and arg: dyn Trait to interact with const fn? If you must specify -> impl const Trait, does it also make sense to require arg: dyn const Trait? Both cases feel more existential-y and thus less tied to the function's type parameters, so I kind of lean that way for consistency.

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Feb 5, 2019

I imagine it makes most sense to use the same (syntactic) rules for impl Trait and dyn Trait as for parameter bounds: that is, in const fn, an impl Trait actually means impl const Trait/const impl Trait (and similarly for dyn) and impl ?const Trait/?const impl Trait allows opting-out of constness.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

mark-i-m commented Feb 6, 2019

I don’t think this has been mentioned before, but putting const before impl would be more consistent with other modifiers like unsafe and pub if we decided to do that.

Drop the correct type
Co-Authored-By: oli-obk <github35764891676564198441@oli-obk.de>
@ExpHP

This comment has been minimized.

Copy link

ExpHP commented Feb 6, 2019

I'm not sure why const Drop must be recursive on fields. In my mental model, the body of drop doesn't have calls to the fields' drop impls inserted, but rather, it's the role of drop glue to piece them all together:

{
    let local = get_thing();

    // Implicitly inserted
    // (each one is individually omitted if that type
    //  does not explicitly implement Drop)
    Drop::drop(&mut local);
    Drop::drop(&mut local.field_1);
    Drop::drop(&mut local.field_2);
}

Requiring const Drop on fields increases churn when library A cannot write const Drop yet due to a non-const Drop in library B. (so to use A in a const context, changes now need to be made to both library B and A in that order, instead of just B).


(one could argue that allowing impl const Drop in library A when a field is non-const Drop could lead to misleading documentation; but another thread of discussion on this PR seems to be arriving at the conclusion that we additionally require an auto-trait like ConstDrop, which partially mitigates this concern)

@oli-obk

This comment has been minimized.

Copy link
Contributor Author

oli-obk commented Feb 10, 2019

I don’t think this has been mentioned before, but putting const before impl would be more consistent with other modifiers like unsafe and pub if we decided to do that.

It has been mentioned before. This was even the original syntax. Reasoning for the change can be found at rust-rfcs/const-eval#8 (comment)

cc @scottmcm for consistency (not in semantics, but user expectations) I have slowly come back around to const impl Trait for Type being the best syntax.

@ExpHP

This comment has been minimized.

Copy link

ExpHP commented Feb 10, 2019

Ultimately the location of const is really nothing but the color of the bikeshed. impl const may be different from the other things, but it is not that hard to remember this "exception" when you have things like T: ?const Trait.

Conceptually, the keyword is attached to the trait. I.e. it's not an impl const of a Trait, but rather an impl of a const Trait.

@Lokathor Lokathor referenced this pull request Feb 11, 2019

Open

Meta tracking issue for `const fn` #57563

0 of 15 tasks complete

@Centril Centril self-assigned this Feb 14, 2019

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented Feb 14, 2019

Conversely, as far as I'm aware, impl const is not consistent with any existing syntax in the language or this RFC.

I think that's looking at it from the wrong perspective.

It's not that it's (impl const) Trait, but that it's impl (const Trait), the same as you can impl (dyn Trait). And that's consistent with something like struct Foo<T: const Default>(T); or similar.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

mark-i-m commented Feb 14, 2019

But as far as I can tell, the Constness is not a property of the Trait, but of the impl, which is not the same for dyn, right?

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Feb 14, 2019

It's not that it's (impl const) Trait, but that it's impl (const Trait), the same as you can impl (dyn Trait). And that's consistent with something like struct Foo<T: const Default>(T); or similar.

I agree with this in particular when viewed in light of effect systems. Moreover, impl const Trait also has composability benefits, e.g. arg: impl Add + ?const Clone + const Debug vs. arg: const impl Add + Cone + Debug are probably not the same semantically.

But as far as I can tell, the Constness is not a property of the Trait, but of the impl, which is not the same for dyn, right?

Traits can be seen as logical requirements and implementations are proofs that those requirements are satisfied. In that light, e.g. T: const Foo can be seen as a constraint that T satisfies const Foo and impl const Foo for MyT is a witness for that.

@Centril Centril added the A-effects label Feb 16, 2019

Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Apply suggestions from code review
Co-Authored-By: oli-obk <github35764891676564198441@oli-obk.de>
@Coder-256

This comment was marked as off-topic.

Copy link

Coder-256 commented Mar 12, 2019

@varkor

const functions will not be called implicitly at compile-time

I was under the impression that const functions would always be implicitly evaluated at compile-time when given const parameters. Does that only apply to variables declared with const rather than let? What about expressions? For example let result = foo(bar(baz)); where the variable baz and the function bar are both const; would bar(baz) be const or not?

@rpjohnst

This comment was marked as off-topic.

Copy link

rpjohnst commented Mar 12, 2019

I was under the impression that const functions would always be implicitly evaluated at compile-time when given const parameters.

That is incorrect. They will only be evaluated at compile time when called from a const context. For example, const FOO = foo() will evaluate foo() at compile time, but let bar = foo() will not, no matter how const its arguments are.

@Coder-256

This comment was marked as off-topic.

Copy link

Coder-256 commented Mar 12, 2019

@rpjohnst Ok that makes a lot of sense, thank you for clarifying. One final question: if you were to disable all compiler optimizations (ex. -O0 for LLVM), is it guaranteed for future versions of Rust, that Rust will never evaluate const functions at compile-time? (again, this is a very very edge-case scenario, but still important to know).

@Centril

This comment was marked as off-topic.

Copy link
Contributor

Centril commented Mar 12, 2019

@Coder-256 I'm hesitant to make such guarantees for all time; but e.g. if a function may panic and we move that to compile time it would be a compile time error which seems like an unlikely change. But we could prove that it will terminate and then execute it at compile-time... Why is the guarantee not to important?

@Coder-256

This comment was marked as off-topic.

Copy link

Coder-256 commented Mar 12, 2019

@Centril It's 100% an edge case, so it's not super important for most code, but for example, benchmarking code would be a good place to have that guarantee. Although that may be out of the scope for this RFC since it would not break anything.

Also, sorry for the barrage of questions, but wouldn't this use of the const keyword for variables be a breaking change?:

const fn foo() {
  const result = bar();
  result
}

Here's what the current Rust book says:

constants may be set only to a constant expression, not the result of a function call or any other value that could only be computed at runtime.

However, if foo() is called from outside a const context, then wouldn't that statement be false for const result? Sorry if this is a bad question, I am just finding it difficult to figure this out myself.

@rpjohnst

This comment was marked as off-topic.

Copy link

rpjohnst commented Mar 12, 2019

In practice, at least for the near to mid-future, any non-const-based compile-time evaluation happens in LLVM, in the form of constant propagation and dead code elimination (and other optimizations).

If you need to guarantee that something happens at runtime (like for benchmarking), you thus cannot rely on anything to do with const or the optimization level. Instead you must rely on something like test::black_box. Work toward stabilizing this is happening in #2360.

However, if foo() is called from outside a const context, then wouldn't that statement be false for const result? Sorry if this is a bad question, I am just finding it difficult to figure this out myself.

This is why the RFC went with the syntax const fn f<T: Trait> instead of T: const Trait- so that you could write your function foo by using that syntax:

const fn foo<T: const /* <- this is extra, beyond the RFC */ Trait>() {
    const result = T::bar(); // this is now allowed, even when `foo` is called from a non-const context
    result
}
@Lokathor

This comment was marked as off-topic.

Copy link

Lokathor commented Mar 12, 2019

@rpjohnst at what point, if ever, could const things be compile time evaluated even when bound with let instead of const? Because that is a totally non-obvious optimization difference that I'm sure will bite countless programmers.

@Ekleog

This comment was marked as off-topic.

Copy link

Ekleog commented Mar 12, 2019

@Lokathor All the time: https://play.rust-lang.org/?version=stable&mode=release&edition=2018&gist=e91e27e7996a664fb38290bcc0c78207 (select “ASM” near the “Build” button)

@Lokathor

This comment was marked as off-topic.

Copy link

Lokathor commented Mar 13, 2019

Except that they specifically said in a comment above that such a thing isn't guaranteed. So it might work for any particular simple example, but that's not at all assured.

@rpjohnst

This comment was marked as off-topic.

Copy link

rpjohnst commented Mar 13, 2019

What exactly are you asking? We interpreted you question as "when is compile-time evaluation allowed in a let binding?" Are you instead asking "when could the language start guaranteeing compile-time evaluation in a let binding?"

If so, I suspect the answer is "never." We have const items for that, and we could potentially add const { .. } blocks, but there's no real need to add an additional guarantee that some_const_fn(some_const_val) is always evaluated at compile time.

@Lokathor

This comment was marked as off-topic.

Copy link

Lokathor commented Mar 13, 2019

Yeah I was asking about the second thing.

If

let a = some_const_fn(some_const_val);

Is ever different from

const c: whatever = some_const_fn(some_const_val);
let a = c;

Then that just seems like a big performance/ergonomics footgun.

The shorter, easier to type version should, all else being equal, always be just as performant as the long winded version. We don't need that guarantee immediately, but we should strive to attain that guarantee eventually.

@rpjohnst

This comment was marked as off-topic.

Copy link

rpjohnst commented Mar 13, 2019

We don't need that guarantee immediately, but we should strive to attain that guarantee eventually.

No, we should not, for the same reasons that we don't infer const-ness on functions. If you need the performance guarantee, you should request it, otherwise you could lose it without any indication that anything has changed. We should resolve any confusion people have by providing better documentation, not by changing the language in a way that makes programs harder to reason about.

It's also not straightfoward whether compile-time evaluation is always better for performance! The default choice of letting the optimizer decide is probably a safer bet- its heuristics can be improved without modifying program source.

@Coder-256

This comment was marked as off-topic.

Copy link

Coder-256 commented Mar 13, 2019

@Ekleog I'm no expert, but I'm pretty sure that's because of optimization. Change Release to Debug and you'll see what I mean. Optimization and const are different things, the former is automatic by the compiler and the latter is enforced by Rust.

@rpjohnst Just to confirm, in my example, const result = bar() would actually produce varying results at runtime? I think this might be a better example:

const fn add_const_test<T: Add>(a: T, b: T) -> T {
  const result = a + b;
  result
}

fn foo() -> u8 {
  add_const_test(generate_random_u8() / 2, 5)
}

Even though we write const result = a + b, wouldn't that not be a compile-time constant when called from foo()? I'm pretty sure that's not allowed currently because declaring a variable with const means it has to be a compile-time constant if I'm not mistaken; but in this case, result cannot be determined at compile-time. Wouldn't that change the statement from the Rust book that I quoted before:

constants may be set only to a constant expression, not the result of a function call or any other value that could only be computed at runtime.

I don't think that it's a breaking change (since it is additive), but it does redefine existing usage of the const keyword for declaring variables. What I mean to say is: I think now we're defining const x to mean that x can only be set to the result of a const fn or some other const value, which is different from the old definition of const x, which I think was mostly meant only for primitives like pi or zero. I could be wrong though.

@rpjohnst

This comment was marked as off-topic.

Copy link

rpjohnst commented Mar 13, 2019

Yes, though that change has already happened- you can already write const Foo: T = some_const_fn() today.

@Coder-256

This comment was marked as off-topic.

Copy link

Coder-256 commented Mar 13, 2019

@rpjohnst Ok thank you for clarifying that. In that case I'm pretty sure that the book needs to be updated... should I open an issue there?

@Ekleog

This comment was marked as off-topic.

Copy link

Ekleog commented Mar 13, 2019

@Coder-256 They definitely are different, indeed. The question I gave an answer to was “When is the compiler allowed to compile-time-evaluate in non-const contexts?”, to which the answer is “All the time” (due to optimization). It sounds like the question was actually “When is the compiler forced to optimize in non-const contexts”, to which the answer @rpjohnst gave is “Never”. :)

@Lokathor

This comment was marked as off-topic.

Copy link

Lokathor commented Mar 13, 2019

@rpjohnst can you give an example of a compile time evaluation of an expression hurting runtime performance compared to that expression being computed at runtime? I mean obviously it will hurt compile time, but when could it hurt runtime performance to enforce that an expression is evaluated at compile time?

@rpjohnst

This comment was marked as off-topic.

Copy link

rpjohnst commented Mar 13, 2019

I'm thinking along the same lines as generic function monomorphization- there's a space/time tradeoff here, and sometimes choosing to use more space unfortunately comes back around and slows down the program.

Doing too much compile-time evaluation could have the same effect- binary bloat by storing lots of constants in memory that could just be computed on the fly when used.

@RalfJung

This comment was marked as off-topic.

Copy link
Member

RalfJung commented Mar 13, 2019

The question I gave an answer to was “When is the compiler allowed to compile-time-evaluate in non-const contexts?”

Note that that question is entirely off-topic for this RFC. You are discussing compiler optimizations here. The compiler will transform your program in arbitrary ways if it thinks that helps with performance, and it will promise not to change the programs observable behavior in the process. Rust makes no promise of optimizations happening or not happening.

This RFC is not at all about optimizations. CTFE and const propagation are almost entirely unrelated, at least conceptually. Please let's stick to CTFE here, and discuss const propagation elsewhere.

@Lokathor

This comment was marked as off-topic.

Copy link

Lokathor commented Mar 13, 2019

They had a simple and honest misunderstanding about a question that was about compile-time function evaluation.

@RalfJung

This comment was marked as off-topic.

Copy link
Member

RalfJung commented Mar 13, 2019

And then instead of clearing up the misunderstanding this derailed into more than 10 posts (and counting) discussing whether const propagation is guaranteed to happen or not, whether it can have adverse side-effects, ... all of which are off-topic.

And before this derails into a meta-discussion about this, I'll stop responding on that subject.

@oli-obk

This comment has been minimized.

Copy link
Contributor Author

oli-obk commented Mar 19, 2019

@rust-lang/lang can we move this to FCP? There has been no (on-topic) discussion that lead to nontrivial changes since a month. All the open questions are documented under the "Unresolved questions" section and should be addressed before stabilization, but should not block implementing the feature unstabily.

@Centril Centril added the I-nominated label Mar 26, 2019

@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Mar 28, 2019

One possible syntax for trait default method bodies being const-capable would be const trait Foo { fn bar() { ... } }.

@joshtriplett

This comment was marked as off-topic.

Copy link
Member

joshtriplett commented Mar 28, 2019

Also, I'm a bit confused about the use of Cell as an example, as it doesn't make sense to me to allow Cell in a const fn. That's not directly an issue with this RFC, though.

@Lokathor

This comment was marked as off-topic.

Copy link

Lokathor commented Mar 28, 2019

If it's a const Cell then there's no reason to disallow it.

@joshtriplett

This comment was marked as off-topic.

Copy link
Member

joshtriplett commented Mar 28, 2019

@Lokathor This seems like something we should discuss in a separate issue rather than this one; I can easily see how this would balloon into a long discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.