HIR typeck: use correct expectations for negation operators#151539
HIR typeck: use correct expectations for negation operators#151539dianne wants to merge 2 commits intorust-lang:mainfrom
Conversation
| let result = self.check_user_unop(expr, oprnd_t, unop, expected_inner); | ||
| let result = self.check_user_unop(expr, oprnd_t, unop, expected); | ||
| // If it's builtin, we can reuse the type, this helps inference. | ||
| if oprnd_t.is_integral() || *oprnd_t.kind() == ty::Bool { oprnd_t } else { result } | ||
| } | ||
| hir::UnOp::Neg => { | ||
| let result = self.check_user_unop(expr, oprnd_t, unop, expected_inner); | ||
| let result = self.check_user_unop(expr, oprnd_t, unop, expected); |
There was a problem hiding this comment.
The expectation this passes to FnCtxt::lookup_op_method is supposed to be that of the result of the operation. It's also ignored for unary operators, so this change has no effect, but in principle I believe it should be expected rather than expected_inner.
| if! { | ||
| if! ( | ||
| match! ( | ||
| break! { | ||
| return! { | ||
| break! ( | ||
| return! ( |
There was a problem hiding this comment.
These needed to be changed to not be blocks to avoid LUB coercions introducing unconstrained inference variables, now that the expectations on the results from the if condition and return type aren't guiding inference. The negation operand in the match scrutinee is already parenthesized rather than a block for the same reason; there's no expectation to guide it.
|
This will need a crater run and an FCP. I imagine the lang team will want to look at it as well, since it affects Rust's static semantics. Due to the nature of the change and particularly the breakage though, I feel like it's probably first and foremost of types team concern, so I'd like T-types to take a look before going through the usual language change process. I feel pretty confident in this change itself being a step in the right direction, but the fallout due to the interaction with But also it's quite possible this doesn't break any code in practice. Anyway, I'm not sure what the process is for type system changes, so let's start with r? types |
| // Since `{ return }` performs never-to-any coercion to a fresh inference variable, its type at | ||
| // that time is that inference variable, rather than `!`, meaning we can't negate it. `!` has a | ||
| // `Not` impl, so if the pre-fallback type of `{ return }` was `!`, this would compile. | ||
| match !{ return } {} |
There was a problem hiding this comment.
Where does this type annotations needed come from? I would expect us to wind up with a registered goal such as ?diverging: Not and then have that go from ambiguity to passing with ?diverging=! after fallback occurs 🤔
Are we structurally resolving the type of the match scrutinee somewhere so we never get to never type fallback? Though, tbh, I don't entirely know what the rules of never type fallback are :)
There was a problem hiding this comment.
We structurally resolve the type of the negation operator's operand, so we never get to never type fallback. Negation operators could use try_structurally_resolve_type instead, but getting to never type fallback isn't as helpful as it could be. e.g. after this PR, if !{ return } {} would still fail to type-check; the !{ return } would have type ! after fallback, so we'd get a type error because the condition was expected to be a bool and but it's a !, since post-fallback we don't do the never-to-any coercion that would otherwise happen there.
| let expected_inner = match unop { | ||
| hir::UnOp::Not | hir::UnOp::Neg => expected, | ||
| hir::UnOp::Deref => NoExpectation, | ||
| // To get better diagnostics for negative numeric literals, use the expected result type as |
There was a problem hiding this comment.
I'm having a difficult time convincing myself this is true. We use the expectation to guide the type of literals. So if we have -1 that could have either NoExpectationor an expectation of, say, usize. This would then affect the Neg goal we wind up registering (either usize: Neg or ?int: Neg).
Given that the resulting type of -1 is going to be <usize/?int as Neg>::Output we're "preventing" inference from flowing backwards from knowing the type of -1 to knowing the type of 1. I.e. when doing <?int as Neg>::Output eq usize I don't think we'll infer ?int eq usize?
If you remove this logic and then do let _: usize = -1; does that get a type inference error? if not can you look into where/why the inference variable for the type of 1 gets resolved.
There was a problem hiding this comment.
Ah though you say:
We structurally resolve the type of the negation operator's operand
so maybe more simply, does let _: usize = -1; just get a type inference error from being unable to structurally resolve the type of 1?
There was a problem hiding this comment.
We use the expectation to guide the type of literals. So if we have
-1that could have eitherNoExpectationoran expectation of, say,usize. This would then affect the Neg goal we wind up registering (eitherusize: Negor?int: Neg).
Guiding inference early is exactly what makes the diagnostics better, yeah. I'll make the comment more precise about that. We do still happen to guide inference if the operand is numeric: we return the operand type as the type of the operation in that case; this means the int infer var can be unified with the expected type of the operation. But we have diagnostics that only work if we get an error in lookup_op_method, which only works if we have a concrete int type. Since ?int: Neg is ambiguous, that doesn't fail, so we get normal trait selection diagnostics for usize: Neg failing later instead of the specialized diagnostics for negation.
If you remove this logic and then do
let _: usize = -1;does that get a type inference error? if not can you look into where/why the inference variable for the type of1gets resolved.
It's a trait solving error: the trait bound `usize: Neg` is not satisfied, which funnily suggests writing let _: usize = -5isize;. check_expr_unop returns the type of the operand, which then gets unified with usize due to the type annotation. If it wasn't constrained, it'd fall back to i32, so I don't think we'd see inference errors (the structurally_resolve_type in check_expr_unop is fine with it since it permits unresolved int vars).
There was a problem hiding this comment.
Ah though you say:
We structurally resolve the type of the negation operator's operand
so maybe more simply, does
let _: usize = -1;just get a type inference error from being unable to structurally resolve the type of1?
It would if structurally_resolve_type didn't permit unresolved numeric vars. We can currently write
let x = -1;which is totally fine and falls back to x: i32 if it's not constrained before fallback.
There was a problem hiding this comment.
An attempt at clarifying the comments to be more specific about how and why it's helping inference: (diff)
|
Process wise this would just be a crater run + T-types FCP |
d6fdfca to
3933e26
Compare
|
The next step here would be the aformentioned crater run @rustbot author |
|
Reminder, once the PR becomes ready for a review, use |
|
@bors try |
This comment has been minimized.
This comment has been minimized.
HIR typeck: use correct expectations for negation operators
|
@craterbot check |
|
👌 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
|
🚧 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
|
Apparently the bug this fixes was also reported as #114380. I've updated the description to close it too. |
|
🎉 Experiment
Footnotes
|
|
Ooh, that's really bad. I doubt there's a pretty fix, too. Maybe this is something that we'd need to patch up over an edition? I'll think about it some more. Most of the failures look like There's also at least one |
|
Thinking about this a bit more—maybe an FCW could work? I imagine we'd take a snapshot and try to use the new typing rules for negation operators; if the operand type can't be structurally resolved after that, we roll back, use the old typing rules, and issue an FCW if we do manage to resolve to a non-type-variable. It's hacky, admittedly. Relying on how it's currently an error if the operand type can't be resolved means it'd probably block #26830 until the FCW can be removed. But it'd mean we could get rid of the current behavior for negation eventually (hopefully). Maybe there's a more clever way to do an FCW here that's more efficient or non-blocking, but my feeling is it'd be best to keep it simple if possible. |
Previously, type-checking logical and numeric negation operators would check the operand with the same expected type as that of the operation as a whole, which could result in type errors for
NotandNegimpls where the operand and result types differ. This PR removes the expectation on the operand type except in the case of negative numeric literals, where it's known to be the same as the result type and having the expectation helps with diagnostics.Fixes #114380. Fixes #151202. The following code now type-checks successfully:
This is a breaking change! By no longer misleading inference, some currently-well-typed code will encounter errors due to unconstrained inference variables. An example of that is the expression
!{ return }, reduced from one of our lovely weird-exprs.rs tests. Currentlyif !{ return } {}types successfully, since the block is checked against an expected type ofbool. Now, it fails to type: the (pre-fallback) type of the block is an unconstrained inference variable due to never-to-any LUB coercion, so type-checking the negation operator fails, since we require the operand type to be known at that point. This is consistent withmatch !{ return } {}, which also fails currently, for the same reason.if !(return) {}andmatch !(return) {}are both fine both before and after this change, since there's no LUB coercion site to introduce an unconstrained inference variable; the type ofreturnon its own is!, which has aNotimpl.