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

Are (non-free) generic constants guaranteed to be evaluated? #112090

Closed
joshlf opened this issue May 30, 2023 · 23 comments · Fixed by rust-lang/reference#1497
Closed

Are (non-free) generic constants guaranteed to be evaluated? #112090

joshlf opened this issue May 30, 2023 · 23 comments · Fixed by rust-lang/reference#1497
Labels
A-const-eval Area: Constant evaluation (MIR interpretation) T-opsem Relevant to the opsem team

Comments

@joshlf
Copy link
Contributor

joshlf commented May 30, 2023

Reposted from rust-lang/unsafe-code-guidelines#409 at the suggestion of @digama0

Thanks to this suggestion by @gootorov, we're considering doing something like the following in zerocopy:

pub trait MaybeTransmutableInto<T: FromBytes>: Sized + AsBytes {
    const REF_TRANSMUTABLE_INTO: () = assert!(
        mem::size_of::<Self>() == mem::size_of::<T>()
            && mem::align_of::<Self>() >= mem::align_of::<T>()
    );

    fn transmute_ref_into(&self) -> &T {
        let _: () = <Self as MaybeTransmutableInto<T>>::REF_TRANSMUTABLE_INTO;
        unsafe { mem::transmute(self) }
    }
}

The nightly reference guarantees that "free constants are always evaluated at compile time." However, that's not quite what we have here - we evaluate the constant REF_TRANSMUTABLE_INTO but assign it to a runtime variable (let _: () = ...). If we replaced let _ with const _, that would be a free constant, but it would never compile (even when we wanted it to) because a constant in a function body isn't allowed to reference types which are generic in that context.

What I want to know is: Is this constant guaranteed to be evaluated so long as transmute_ref_into is used?

@CAD97
Copy link
Contributor

CAD97 commented May 30, 2023

It's unambiguously guaranteed that at a bare minimum, the code will panic before executing the transmute. This issue is about canonicalizing whether the panic is allowed to happen at runtime or whether it must happen at monomorphization time and cause a compilation error which prevents emission of an executable.

#107503 et. al. is related but distinct, about defining when a monomorphization is "used."

If this spelling is permitted to panic at runtime, it would be (will be) possible to place the constant instantiation in a const block, which is specified to force constant/compile-time evaluation. (Since const blocks and associated const items share much of the same machinery, in practice it and an instantiation of an associated constant should behave the same.)

my well established position

I've made my position clear w.r.t. monomorphization instantiation const evaluation errors; all constant evaluation required to fulfill statically instantiated monomorphizations should semantically be evaluated, and skipping nonpure evaluation should be considered incorrect.

I would also accept changing it such that const evaluation still semantically happens at the point of use, meaning that the unwind occurs when the const is used and the compile error is only a deny-by-default (but suppressable) guaranteed_panic lint.

The current status that associated const evaluation may semantically happen at a compile-time item context (and cause a compilation error if it panics) or at runtime expression context (if the code is proven always dead and never monomorphized, thus not exposing any const panics) is IMHO untenable.

You can argue that const evaluation is still guaranteed to happen at a compile-time item context, but to do so you need to make some provision for const items which may or may not be evaluated in order to permit the current behavior.

@RalfJung
Copy link
Member

To my knowledge, we guarantee that every constant mentioned syntactically within a function that executes at runtime, has been previously executed at compiletime. (This is certainly current compiler behavior, and we explicitly have code to make this happen, it's not just an accident of the current implementation.) I don't know if and where we document that guarantee, though.

@joshlf
Copy link
Contributor Author

joshlf commented May 31, 2023

To my knowledge, we guarantee that every constant mentioned syntactically within a function that executes at runtime, has been previously executed at compiletime. (This is certainly current compiler behavior, and we explicitly have code to make this happen, it's not just an accident of the current implementation.) I don't know if and where we document that guarantee, though.

That's good to hear! A few questions:

  • What does "mentioned syntactically" mean here? Does this contrast with some other notion of, e.g., "mentioning semantically" or somesuch?
  • IIUC, "that executes at runtime" is the important qualifier here because code which does not execute at runtime may not get executed at compile time (e.g., Const-eval errors in dead functions are optimization-dependent #107503), right?
    • Should I read "within a function" to imply that, so long as the constant is mentioned anywhere in such a function, it's guaranteed to be executed at compile time? E.g., the following code is guaranteed to panic?
    const X: () = { panic!(); };
    
    fn main() {
      if false {
        // This branch may be dead code eliminated, but `X` is definitely evaluated at runtime
        // because it appears inside `main`, which executes at runtime
        let _ = X;
      }
    }
  • Do you have a sense of where the appropriate place would be to document this behavior?

@CAD97 if I'm reading your comment correctly, you're of the same opinion as @RalfJung? I quoted their comment because it contains language I wanted to respond to directly, but really I think I'm responding to both of your comments, which seem to agree with each other.

@digama0
Copy link
Contributor

digama0 commented May 31, 2023

IIUC, "that executes at runtime" is the important qualifier here because code which does not execute at runtime may not get executed at compile time (e.g., #107503), right?

Here's one way to think about it: imagine the const body was inlined, and ask whether that line of code is reachable by any possible runtime arguments. If it is, then the const body must be evaluated, and a const body that must be evaluated must be evaluated at compile time.

I think we would like to strengthen this guarantee to something more syntactic, i.e. if there is a reference to a const in the function at all, and the function is monomorphized, then the const is also monomorphized, and a monomorphized const is executed at compile time. However, this behavior is in contradiction with #107503 so we will have to fix that first.

For your example, we get the following:

  • In the compiler currently, X is not evaluated (or at least not reliably evaluated), due to Const-eval errors in dead functions are optimization-dependent #107503.
  • Because it is in a if false branch, the line is not reachable and hence the "weak guarantee" I gave in the first paragraph does not apply.
  • The "strong guarantee" of the second paragraph does apply, because X is mentioned inside main and main is monomorphized because it is main, so X is monomorphized, evaluated, and a panic occurs at compile time.
  • Nothing inside a const body (or const block) ever evaluates at runtime. If it is run, it must be run at compile time.

@joshlf
Copy link
Contributor Author

joshlf commented May 31, 2023

Okay, in that case I'm pretty sure I understand the current state of affairs.

Nothing inside a const body (or const block) ever evaluates at runtime. If it is run, it must be run at compile time.

Is this guaranteed by the reference?

@digama0
Copy link
Contributor

digama0 commented May 31, 2023

From https://doc.rust-lang.org/reference/const_eval.html#constant-expressions:

Certain forms of expressions, called constant expressions, can be evaluated at compile time. In const contexts, these are the only allowed expressions, and are always evaluated at compile time.

@RalfJung
Copy link
Member

RalfJung commented May 31, 2023

What does "mentioned syntactically" mean here? Does this contrast with some other notion of, e.g., "mentioning semantically" or somesuch?

I specifically meant that even constants inside an if false are evaluated. So it doesn't depend on whether any particular point in the function is ever executed at runtime. It just means that CONST appears as a subexpression anywhere in the function's body (excluding nested items).

IIUC, "that executes at runtime" is the important qualifier here because code which does not execute at runtime may not get executed at compile time (e.g., #107503), right?

Yes. We actually evaluate consts for all functions that are monomorphized, but we don't have clear guarantees on what does or does not get monomorphized, as indicated by the issue you linked. However if a function actually executes at runtime then surely it has been monomorphized.

(Crucially, the "that" above refers to the function containing the use of the const", not just any particular line in that function.)

Do you have a sense of where the appropriate place would be to document this behavior?

Hm, I guess either the const keyword docs or the reference?

@digama0 you are stating weaker rules than I am.

In the compiler currently, X is not evaluated (or at least not reliably evaluated), due to #107503.

This is not correct. X is evaluated. For constants inside a function, we collect a list of all of them before doing any optmizations, and we eval all of them even if the code they appeared in is eliminated. It is only for functions called within a function that whether or not they get monomorphized depends on optimizations.

Put differently: X is syntactically mentioned in main and main gets executed, hence X gets evaluated.

@digama0
Copy link
Contributor

digama0 commented May 31, 2023

@digama0 you are stating weaker rules than I am.

Yes, I gave two versions of the rules that I called the "weak guarantee" and the "strong guarantee". My understanding is that the rule you are giving is what I called the strong guarantee, and the #107503 behavior lies somewhere in between.
It's a bit awkward to claim that rust guarantees the strong behavior even though rustc currently demonstrably doesn't and a fix is likely still some ways off.

In the compiler currently, X is not evaluated (or at least not reliably evaluated), due to #107503.

This is not correct. X is evaluated. For constants inside a function, we collect a list of all of them before doing any optmizations, and we eval all of them even if the code they appeared in is eliminated. It is only for functions called within a function that whether or not they get monomorphized depends on optimizations.

Ah, thanks. I saw that there was some inconsistency in whether const code was getting evaluated but I wasn't sure what the exact description was, I didn't realize that out-lining via a const fn made a difference.

@RalfJung
Copy link
Member

RalfJung commented May 31, 2023

Out-lining via fn is what makes the difference; the functions in question for #107503 are not called in const context.

Also I am still confused by this:

I think we would like to strengthen this guarantee to something more syntactic, i.e. if there is a reference to a const in the function at all, and the function is monomorphized, then the const is also monomorphized, and a monomorphized const is executed at compile time. However, this behavior is in contradiction with #107503 so we will have to fix that first.

Your paragraph describes current behavior exactly and is not in contradiction with #107503. #107503 affects functions called from dead code, but not consts called from dead code in a live function.

@digama0
Copy link
Contributor

digama0 commented May 31, 2023

Your paragraph describes current behavior exactly and is not in contradiction with #107503. #107503 affects functions called from dead code, but not consts called from dead code in a live function.

I suppose the unstated part of that sentence is that functions are also monomorphized when they are referenced in a function that is itself monomorphized, because we would like this "monomorphized by" relation to be transitive. So if a ODR-used function calls a function which has a const block, then the const block is monomorphized and hence evaluated.

@RalfJung
Copy link
Member

RalfJung commented May 31, 2023

I suppose the unstated part of that sentence is that functions are also monomorphized when they are referenced in a function that is itself monomorphized,

That part is the one which doesn't apply currently, due to #107503.

I don't know what "ODR-used" is/means.

@jyn514 jyn514 added A-const-eval Area: Constant evaluation (MIR interpretation) T-opsem Relevant to the opsem team labels May 31, 2023
@jyn514
Copy link
Member

jyn514 commented May 31, 2023

might be referring to the "static const" section of https://en.wikipedia.org/wiki/One_Definition_Rule ?

@digama0
Copy link
Contributor

digama0 commented May 31, 2023

ODR-used is a technical definition from the C++ spec which basically means "transitively required by monomorphizations leading to some exported function or main". It's how C++ solves this "syntactically referenced by" situation, by defining exactly what kinds of references actually count as a "use" for the purposes of the ODR (one definition rule).

@RalfJung
Copy link
Member

RalfJung commented Sep 6, 2023

Past discussion of this topic: #67191. However, I couldn't find an FCP establishing an explicit T-lang guarantee for certain consts to be evaluated.

@joshlf
Copy link
Contributor Author

joshlf commented Sep 29, 2023

Okay, sounds like it'd be worth documenting that guarantee somewhere.

As a stopgap, is the following guaranteed to execute?

trait Foo {
    const BAR: ();
    const BAZ: bool;
}

impl Foo for () {
    const BAR: () = assert!(...);
    const BAZ: bool = {
        assert!(...);
        true
    };
}

fn foo<F: Foo>() {
    let _ = F::BAR; // might be dead-code eliminated
    assert!(F::BAZ);
}

In the context of foo<()>():

  • I assume that F::BAZ is guaranteed to be evaluated?
  • I assume that the assertion in the assignment of <() as Foo>::BAZ is guaranteed to be evaluated, and that if it fails, that's guaranteed to fail compilation?

@RalfJung
Copy link
Member

If foo gets monomorphized with a particular F (so in particular, if it ever gets executed with a particular F), then both F::BAR and F::BAZ are guaranteed to be evaluated, and if that evaluation fails (e.g., if it panics), then compilation is guaranteed to fail.

@joshlf
Copy link
Contributor Author

joshlf commented Sep 29, 2023

If foo gets monomorphized with a particular F (so in particular, if it ever gets executed with a particular F), then both F::BAR and F::BAZ are guaranteed to be evaluated, and if that evaluation fails (e.g., if it panics), then compilation is guaranteed to fail.

I'm confused; doesn't your reply here imply that F::BAR isn't guaranteed (at least not by FCP) to be evaluated? The reason I assume that, even without FCP, F::BAZ is guaranteed to be evaluated is that Rust promises that assertions are always evaluated, and so it's not allowed to dead-code-eliminate BAZ.

@RalfJung
Copy link
Member

RalfJung commented Sep 29, 2023

If by "guaranteed" you mean t-lang FCP'd, then I don't think we have any guarantees here. ;)

But this is just a procedural matter, I have no doubt that we will have (at least) the guarantee I mentioned. We just need someone to push this through the process.

@briansmith
Copy link
Contributor

How about adding a test to the test suite? I bet lots of people are relying on this already, so it should be tested and guaranteed.

@RalfJung
Copy link
Member

RalfJung commented Oct 5, 2023

I'm pretty sure we have such tests...

@RalfJung
Copy link
Member

RalfJung commented Oct 5, 2023

Turns out we have a test in Miri but I couldn't find one in rustc. So here's one: #116444

@RalfJung
Copy link
Member

Reference PR that should resolve this: rust-lang/reference#1497.

@RalfJung
Copy link
Member

New issue for the t-lang decision: #124971.

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 (MIR interpretation) T-opsem Relevant to the opsem team
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants