-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Errors in constants and promoteds stop compilation (via deny-by-default lint) even in dead code #74696
Comments
For what it's worth: it works fine for me in a nightly playground. I'm assuming the current Edit for future people: see the two answers below :) |
It works because I added |
AHH, I thought that flag was to enable the Here's the playground then, with the error enabled :3 https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=28ae219a9e7d1054cb7009c5bb254686 |
Hm, this is interesting. It feels loosely related to discussions we recently had about constants always having to be well-formed even when unused (Cc #67191). But this time, it also involves promotion -- at least, I think Cc @oli-obk |
There's no hard error involved, so all is fine from that perspective. I guess we'll need some sort of control flow based const prop to not hit these cases |
Promoteds don't usually trigger Remember that |
oh... I guess we should just not propagate const generics at all. We can't enfore wf bounds for promoteds, because they are implicitly generated. This is the same issue we have with associated constants: This happens on stable: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c5377d51d296c31a0cb1284269833bf5 |
I don't see why we wouldn't const-prop them. We just have make the lint less silly. This doesn't look like a const-prop thing at all to me. Const-prop doesn't emit if ... {
const FOO: i32 = something / 0;
FOO
} else { ... } The bug is that here we get this error not for a const but for a promoted -- unlike consts, those do not have to evaluate successfully. I wonder how anon-const-blocks should behave here... Cc rust-lang/rfcs#2920 @ecstatic-morse |
Ugh. Why are words so similar and why do I keep mixing them up. I think we should not promote const generics at all. Const prop is entirely irrelevant here. EDIT: we could promote direct uses of const generics (so |
What is promoting, in this context? Sorry I have to ask, but this topic is very interesting to me and I can't follow what you guys are saying x3 |
Promotion is the action of implicitly generating constants even if the user did not explicitly write anything to that regard. Further reading is available in the reference: https://doc.rust-lang.org/stable/reference/destructors.html?highlight=promotion#constant-promotion |
Ahh I see. So like, say we have a constant and we use it like this: const SIZE : u32 = 10;
fn foo() -> {
let a = [0; SIZE]; // maybe I wrote it wrong, but the idea is that a is an array of 0s of length SIZE.
println!("{}", a);
} Then And then, after promotion, |
@RalfJung I think that maybe the error happens before that borrow. Before |
The error happens at compile-time, i.e., before run-time -- but the order of operations in the code is irrelevant. For more on promotion, also see https://github.com/rust-lang/const-eval/blob/master/promotion.md.
That seems odd though? I mean it's an okay temporary hack, but really these values can be pre-computed so promotion is fine. We just must not
Happy that we agree on that. :) |
So, this lint is emitted here, which is specifically the "emit a lint for promoteds that fail to eval" logic. I guess we just need a new lint for this case? |
@RalfJung you are completely right. Removing the PS: thank you for the link :) |
I think the logic itself might be wrong. In the sense that this expression should never be promoted if |
Promotion cannot know if code will be evaluated or not, so that part is expected. The linting part is interesting though. We don't even need any associated consts / const generics to trigger the problem: fn main() {
if false {
let _val = &(1/0);
}
} The diagnostics here are not great, we actually get three errors: (Cc #61821)
So, yeah, this should probably be a warning, similar to the overflowing arithmetic/bitshift lints. |
Hmm.
I mean it could know. I will be working on control-flow awareness for the const-prop optimization. Do you think it could be adapted/used for making better promoted errors? |
No, it cannot. Not in general. See "halting problem". ;) Sure we could be smarter in some special cases, but honestly I don't see the point of that. Promotion is already a horrible monster, let's not also make it path-dependent or anything like that. |
Aside: correct me if I'm wrong, but I think that this issue is only relevant for non- Unless such a |
A So, no, let's just not go there. Promotion is part of code generation. Code generation should not depend on whether prior code diverges or anything like that. It doesn't solve the problem properly (there'll always be cases we cannot detect, see halting problem) and costs a huge amount of complexity. |
Of course, of course. But it could at least give true errors if it can prove that they will be errors, and issue warnings whenever it can't. That's what I mean ^^. For example, in the OP it could even skip the warnings because it can be proven that no C will break it (up to wf errors). And if you inverted the conditions in the |
Please let's keep this issue focused on what kinds of lints we emit when we get an error while evaluating a promoted. Once we solved that problem and are happy with the solution, maybe we can also additionally consider whether the promoted is in dead code or not -- but considering that does not nothing to simplify the problem here (there'll always be "maybe dead code, maybe not" cases) and only makes things more complicated for no good reason. IOW, your suggestions are scope creep. Currently we cannot even handle the general case properly. So let's fix that before even considering to do something else in special cases. |
I'd phrase it as "I think your suggestions are scope creep".
But isn't precisely the issue about the lints being overly cautious? It says so in the title. Maybe you could open a different issue about the general topic. |
Related: I think this is currently how the bounds that specify const generics parameters' operations well-formedness is going to be handled. By running |
The title was given by the OP and doesn't reflect the actual problem. As has been said above multiple times, the problem is not that the lint is too strict in some particular case or so; there is a fundamental flaw in how we emit the lint around promoteds. We have other lints for arithmetic and bitshift overflows that behave better; failing promoteds should be similar. No "is this code reachable" analysis is required for any of that. I do not understand why you keep moving the discussion elsewhere from that problem. I won't respond to the meta-discussion here any more as it is draining and clearly leading nowhere. |
Because I did not understand the problem. I still don't really understand it. I am definitely missing some context. The problem is more than what it seemed like to me when the issue was opened, and in actuality, the original post can be fixed with both your and my ideas independently, which didn't help me realize this. I didn't do it with the intention to bother you, to derail the conversation or anything like that, and I'm sorry if it seemed that way at first glance 🙂 I'll see myself out. This is not a topic I can help with~ |
(PS: are the current labels alright? Since the real problem is now clearer, maybe the labels to have are different from the ones they were assigned originally) |
Uh, I never know how to label things.^^ And we don't seem to have anything for "trouble around promotion"... amybe we should have "A-promotion" or so? |
Hm, actually, So even independent of any improvements to all of these lints along the lines of what @felix91gr was imagining, we have two issues I think:
For the second point, here's an even more "grey area" example: const C: i32 = 0;
fn main() {
if C != 0 {
let _val = 1/C;
}
} (@felix91gr also suggested having a fancy analysis that affects even whether we promote; I do not think that is something we should ever do. The decision to promote happens very early, before even MIR is constructed, so this would be technically challenging. It is also IMO conceptually a bad approach. My interpretation now is that this proposal was based on not yet having a full overview of how promotion works.) |
I personally think this seems like a good idea, not really sure about const C: i32 = 0;
fn main() {
if C != 0 {
let _val = 1/C;
}
} I think that we should err here as this fails no matter what happens without a breaking change while using const generics or associated const an expression only fails in some cases while succeeding in others |
IMO it would be very strange if my example behaved different from yours. In both cases we first check the value of a constant before using it. Is there any precedent for making the default lint level depend on details of the error situation? FWIW, my preference would be to just make the lints warn-by-default and leave it at that. |
That is not the distinction I am making here, my point is the following: #![feature(const_generics)]
fn foo<const C: u8>() {
if C > 0 {
println!("Foo gives {}", 25 / C);
} else {
println!("Foo gives 0");
}
}
fn main() {
foo::<0>();
foo::<1>();
} Calling const C: i32 = 0;
fn main() {
if C != 0 {
let _val = 1/C;
}
} There is no possible instance of main (considering that it's already fully concrete) in which
It seems quite weird to guard against the value of a local constant here. But yeah, this doesn't seem important enough to warrant this much discussion, so I am also fine with just changing this error to warn by default. |
That's not possible at all in the current setup. We can opt to not emit the lint depending on the situation, so we could decide not to lint if there were any named constants involved (in contrast to constant values from literals) |
Hm, that also sounds like a reasonable idea. Even as a warning it would be quite annoying after all. We should just try to ensure that we don't warn less than we would if things would not have gotten promoted. But then we should do the same for the lints originating from ConstProp: #![feature(const_generics)]
const D: u8 = 0;
fn foo<const C: u8>() {
if C > 0 {
let val = 25/C;
println!("Foo gives {}", val);
} else {
let _val = 25/D;
println!("Foo gives 0");
}
}
fn main() {
foo::<0>();
foo::<1>();
} This says
|
#80579 (if we can land it) will stop promoting division unless we can prove the divisor is non-zero, so the original error here is gone. However, this issue remains -- the unconditional_panic (and arithmetic_overflow) lints are emitted more often than we might like; maybe we should make them smarter and/or warn-by-default instead of deny-by-default. |
@felix91gr is working on that |
Indeed. If the mir-opts are available at this level, you should have better info about this kind of code after I'm done. I'm working on using dataflow for constant folding, which will be able to ignore dead code and in general, be aware of the control flow of the program :) |
I was talking about lints, not constant folding... so I am a bit confused now. Better constant folding sounds great. :) However, I don't think it is a great idea to conflate optimizations and lints too much, that seems extremely fragile. Above we had rather simple proposals like checking if there are named constants involved... what happened to those?^^ |
Ah, my bad. Okay, so if this is happening at the level of lints... (sorry, it's been a while since I read this thread) hmm... If the compiler lints, like some clippy lints do, are using constant folding to know some variable values in advance, then this kind of error should be fixed after my changes are done. Unless the error stems from somewhere else (which it might!) I agree with being tied to optimizations being a fragile thing, however in this case I tend to look at the usage of mir opts in this way: "can I know this value at compile time?". And if the optimizations are sound (which should be the case, otherwise the analysis itself becomes unsound), then I feel it's fine to use them to infer more about the code being processed. |
PS: oh god I'm re-reading this issue and I might've done it again 🤦🏻 sorry @RalfJung, I owe your patience twice now. Okay so what Oli is referring to is that many of those The work I'm doing should hopefully make the const-prop engine much more precise and complete than it is right now, without making it less conservative. So that should help make a lot more things "smarter" in the scenarios posted in this thread. I think there is something else going on there though... since it should never Again Ralf, my apologies. I got things mixed up again. |
That makes sense... but I am not sure yet how it affects examples like this: #![feature(const_generics)]
const D: u8 = 0;
pub fn foo<const C: u8>() {
if C > 0 {
let _val = 25/C;
} else if D > 0 {
let _val = 25/D; // ERROR
} else {
panic!()
}
} Before we said the best approach to avoid these errors is to not lint when the expression involves a named const. Are you saying that, instead, we should rely on the underlying analysis being smarter about detecting dead code? Will that truly be good enough? Lints and optimizations have somewhat dual constraints here... lints should avoid false positives (so "considering more code dead" is okay), but optimizations must never miss a potential case. |
I'm confident that in this example, dataflow-based const prop will ignore the second branch ( This example also shows a soundness issue* with the current const prop engine: a named const should not be propagated into dead code.
I think I see what you mean there. In this context though, I think we're on an edge case of that duality. Since const prop is intended more as process of enrichment of the MIR. I think the only way I can illustrate it is by pointing to lints which require good const-prop to work. Take a look at this one: rust-lang/rust-clippy#628
This seems very valid to me as a workaround while I work on the new implementation! :) |
Why not? |
The reason I'm thinking of is this: Dead code will never happen, and therefore the asserts inside it will never happen either. Yet division, which has an assert inside it ("the divisor is not 0") will be processed by Asserts become meaningless in dead code (right? Unless I'm missing something in the style of Why even unused data needs to be valid). And therefore if we fail an assert in dead code, we're rejecting code that should be accepted otherwise. Propagating named constants into dead code (and indeed, anything into dead code) has the potential to do this, and therefore is something that we'd rather not do. Does that make sense? |
Incorrectly emitting a lint is not a soundness issue though. A soundness issue is when you can cause UB in safe code. Also see the UCG definition of soundness. Sure, emitting lints about dead code might be considered false positives for the lint, but OTOH we do perform type-checking even in dead code and type errors in dead code are not false positives, either. |
Ah, my bad. You're right. The word I should be using is "completeness" instead of soundness, then, right? So it's be like this:
I wonder if there's a fundamental difference in this context between type checking and "value checking" (by which I mean asserts based on values in the program). Because yeah, I think it makes sense to do type checking even for dead code. But the liveness of code is mostly a function of the compile-time available values, right? So maybe they are different... and checking values in dead code can be considered unnecessary? |
Completeness of the lint would be technically correct I think, but to avoid confusion I think it is easier to talk about false-negatives / false-positives of the lint.
Types like |
Also see #81265 for the kinds of problems that are caused by using an optimization pass to emit lints. I am more and more convinced that that's just bad design. It leads to coupling effects that are very hard to control. |
I think this is a valuable feature to have, but it's a pain when it gives false positives, and we should change that. Is there a model or a set of rules that we could use to decide where it makes sense to emit lints? I like the idea of piggybacking on certain optimizations for the purpose of emitting lints, because many of the operations used in the optimizations can also be used to conclude truths about the code. Ideally I'd want them to exist separately, since the overlap of areas might not be the same. (I was gonna mention dead code, but dead code should stop being a problem after I add the non-lexical constprop pass) Edit: sorry, I forgot to add the following 2 paragraphs. So ideally I'd want things to exist separately. It's very useful to have a lint alert you of a division-by-0 that's not obvious when reading the code but becomes clear after processing it in the MIR. This is currently done at the optimization level, by triggering asserts (like divisor is different from 0 in this case) whenever you can evaluate them at compile time. It should be feasible to add this computation and linting process to a different, separate analysis which only looked at code for the purpose of detecting errors and warnings. However, some linting will have to happen at the optimization level, unless we can prove before optimizing that no asserts will be triggered. I think currently we're at the point of "optimization triggers a lot of lints, and some are false positives". I think the next step might be "optimization triggers a lot of lints, which rarely are false positives", and the one after that "optimization almost never triggers lints, and most lints that used to come from opts now come from MIRI-based analyses". |
@lcnr promotion changed a lot since the report was made, and the main example in your post no longer shows an error. The 2nd example needs |
i think that's fine 👍 |
example by @felix91gr
Note that this currently requires
allow(const_err)
because25 /C
would panic at runtime.This is however fine as it isn't reachable because of
C > 0
.We probably should relax this slightly here, not sure what's the best approach though
Other example (by @RalfJung)
The text was updated successfully, but these errors were encountered: