-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Replace our fragile safety scheme around erroneous constants #67191
Comments
Also Cc @Centril -- I am curious about your position on "compilation failure" being a side-effect of CTFE that we want to guarantee is being preserved. On the one hand this seems useful for some things, on the other hand it makes CTFE somewhat impure. Relevant discussions: |
@RalfJung I think I'm lacking some context; could you elaborate re. the various positions one could take and their trade-offs? |
Essentially the question boils down to "is it ok for the following program to never emit a hard error, but at best emit a dead code warning or a const_err lint" const FOO: usize = 0 - 1;
fn main() {
if false {
println!("{}", FOO);
}
} Everything else is just a more complex instance of the shown example. Although some of these situations may in fact be relied upon by users for safety: const FOO: ! = panic!();
fn main() {
let _ = FOO;
unsafe {
*(5 as *mut i32) = 49;
}
} In the above code it can be expected that the unsound code is in fact dead code (as the compiler tells you: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=a2e0b258329500d44609bd82afd8ed07) and it even is unreachable, but only because the |
Hm, that uninhabited type example is interesting, I was actually thinking of inhabited ZSTs so far... there we don't even have much of a choice. We have to abort compilation. (Well we could generate a [safe] trap but that would be very surprising.) Things are less clear in situations like this: const FOO: ! = panic!();
fn main() {
if false {
let _ = FOO;
}
} One could argue that we shouldn't error about bad constants in dead code. Is there a way people could rely on compilation passing when their CTFE error is only used in dead code? |
What's the reason behind having bad constants inside dead code? The only situation I can imagine is some platform specific constant evaluation that would succeed in one platform and fail in another. But even then you could be better using conditional compilation instead. |
@oli-obk @RalfJung So I think I understand the question now, but some elaboration re. "impure" and what drawbacks checking constants in dead code would be good. It sounds like the drawback would be potentially regressing perf and memory usage (but the evaluation of the constants used in live code should be memoized (?) so we would mostly only pay for the ones in dead code? My position for now is that:
|
(Sorry if I'm misunderstanding the question here) From a user's point of view I think control-flow based analyses are confusing. I think in this code const FOO: ! = panic!();
fn main() {
let _ = FOO;
unsafe {
*(5 as *mut i32) = 49;
}
} We should abort compilation. Same in this example const FOO: ! = panic!();
fn main() {
if false {
let _ = FOO;
}
} If you ever worked with a langauge with control-flow based static checking (e.g. RPython) it's really confusing for a user for several reasons:
I'm very new at rustc development but I think compile-time evaluation is done in MIR, which can be optimized. If this is done post-optimizations then we can also add an item here about getting different compile-time feedback depending on optimizations, which is also bad (GHC also has such problems, though they're really minor stuff and most users are probably not even aware of these). |
T-compiler triage: leaving unprioritized until we see feedback from T-lang on what we should do here. |
We discussed this briefly in the language team meeting this Thursday. In this meeting, only @Centril, @nikomatsakis, and @ecstatic-morse attended. Our basic conclusions were that having one consistent standard for "dead" / "live" as a matter of language definition (unaffected by compiler optimizations and lints like We briefly touched upon implementation strategy and wondered whether e.g. collecting things during the NLL analysis would be a good idea. This is of course left open for the compiler team to decide. :) |
Some extra information: it appears that #67164 caused a significant performance regression. (It's not 100% certain, because it was part of a rollup and I am having trouble reproducing the regression locally.) According to this comment, this PR would allow #67164 to be reverted, thus undoing the performance regression. So that increases the urgency of this. |
Will this safety scheme also apply the negative bounds like I assume this kind of bound problem will be triggered before actual control flow checking. Are we going to touch parts that disables this kind of |
triage: P-high to resolve desired semantics and implementation strategy here. (I don't know what priority to assign to the actual implementation work yet though...) |
No, this PR is not about any kind of changes to the way constants by themselves behave. This is solely about the question whether any used constant should cause an error, even if that use is in dead code. Though there's also the question as to what kind of dead code we look at. E.g. is if false {
CONSTANT;
} a use? If yes, is if super_hard_to_const_eval_to_false() {
CONSTANT;
} a use? or panic!();
CONSTANT;
I don't understand the questoin. Can you give an example? |
(According to #67191 (comment) it would be yes, yes, and no.) |
Note that there were examples where it wasn't |
This answers my question actually:
I wanted to know that iirc: derefing this on embedded systems causes abort instr. to execute. That might be useful I assume. |
So, I definitely don't expect a full optimization-based liveness analysis; it seems fine to me if (say) if false { const FOO: ! = panic!(); }
const CONST_FALSE: bool = false;
if CONST_FALSE { const FOO: ! = panic!(); }
const fn const_false_fn() -> bool { false }
if const_false_fn() { const FOO: ! = panic!(); }
const fn const_false_fn() -> bool { false }
if const_false_fn() { return AnError; }
const FOO: ! = panic!(); Rationale: this makes it easy to use patterns similar to those in the Linux kernel, where a module uses compile-time configuration to either provide certain functionality or stub out that functionality, and the stub functionality could use |
I think it would be mighty confusing if whether |
@joshtriplett I feel that Linux's configuration system is an outgrowth of the lack of a good macro system and build tool in C. In rust, I feel that |
@mark-i-m I'm not talking about kconfig. I'm talking about code modules. Suppose you have an API with half a dozen functions and a thousand callers. You can use cfg to define the functions when configured in, or to define stub versions of the functions when not configured in, and either way, you don't need to change the thousand callers and litter myriad source files with additional cfg directives. The stub versions of the functions can be const functions, and any types involved can become ZSTs, so a feature that has been configured out has zero overhead. Depending on the semantics of the feature you compile out, sometimes you want it to return a constant (such as true or false or NULL or ()), and sometimes you want it to error (panic) if ever actually invoked at runtime (and not eliminated by dead code elimination). |
Hmm... I see what your getting at, but isn't that already possible today? Do you need erroneous constants for that? |
I'm just coming back online, but I'm surprised that This does mean that we can no longer optimize by skipping evaluation of unused constants, but this particular optimization is not very important IMO. We should be discussing the extension of the aforementioned rule to associated constants and constants in a generic context, e.g. |
Ah, nice! #67134 also does some changes in |
Reopening since there are still some leftovers as noted in #67191 (comment) |
Also see the Zulip discussion for this (which is still ongoing) |
Changed the priority, this doesn't seem to be |
After reading around some more, I think I have the following two action items / open questions for this issue:
|
#71747 takes care of item 1 (the FIXME). |
I don't think we should remove item 2. As mentioned on zulip, we do want to trigger these lints on generic code and not just during codegen/monomorphization. |
I agree we want to get these lints e.g. also for |
Also it seems like we have quite a bit of code duplication here: both codegen and const_prop contain logic that turns const errors into hard errors. That code should be shared. Moreover, the diagnostics we give with |
Const prop evaluates only those that it needs for propping, though with the current scheme I think we can actually stop doing that as we know they are either unevaluable (because they are in the There's no point in trying to evaluate those in
Remember
|
That's not
That's what I meant. There even is precedent for making future-incompat lints hard errors on earlier editions. But anyway, if you, too, are in favor of this path -- should I open an issue to track that?
So now you are saying we can indeed remove this or am I misunderstanding? |
oops yea
yes, I really want that.
That highlight is pointing at a weird subset of code, I presume you mean rust/src/librustc_mir/transform/const_prop.rs Lines 418 to 427 in dae90c1
which we can't remove (because we also use it for rust/src/librustc_mir/transform/const_prop.rs Line 412 in dae90c1
), but can seperately do a future incompat lint for just the generic part. We want to lint/error here, because while it's guaranteed that we error during monomorphization, monomorphization may happen only in a crate depending on the currently compiling crate. But we can probably move it to
MutVisitor that evaluates all constants that it can. This way we never have to evaluate another constant in the entire MIR optimization pipeline, because we know none can be evaluated.
EDIT: I thought we were already evaluating them during collection, but that's not the case, thus my statement
is wrong right now, but easily amended to be right. |
Let's get the ball rolling then: #71800
So this is a bit like the "unused broken consts" lint situation?
We already have such logic in
Yes I was imagining something like that, I think.
Looks like we got another concrete action item then? |
Good question, I don't remember the test that this touched. Maybe I should have referenced it in the comment :/
Another good point. We may be able to just not report anything for promoteds here, maybe that's one of our dupliation sites?
Yes: #71802 |
Okay, looks like all the const-eval error reporting cleanup that we might be able to do is now tracked at #71802. Closing this one. |
Discussion: #67134 (comment)
The situation is as follows:
()
)The proposed solution is to gather all unevaluated constants in a MIR right after mir building and put them in a
Vec<(DefId, SubstsRef<'tcx>, Promoted)>
(or maybe aHashSet
?). This should be placed on themir::Body
and inlining should also carry it down to the function that its being inlined into (and adjust substitutions on theSubstsRef
). In the end, instead of having the collector go through the MIR to find constants to evaluate, we go through the vector and evaluate all of them.This will require more memory and some additional evaluation time, but it would be significantly less fragile than the current setup which relies on optimizations not removing even guaranteed dead code containing constants.
cc @RalfJung @wesleywiser
The text was updated successfully, but these errors were encountered: