Skip to content

HIR typeck: use correct expectations for negation operators#151539

Open
dianne wants to merge 2 commits intorust-lang:mainfrom
dianne:unop-expectations
Open

HIR typeck: use correct expectations for negation operators#151539
dianne wants to merge 2 commits intorust-lang:mainfrom
dianne:unop-expectations

Conversation

@dianne
Copy link
Contributor

@dianne dianne commented Jan 23, 2026

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 Not and Neg impls 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:

fn main() {
    // These would previously error, since the `&i8`s were forced to coerce to `i8`.
    let _: i8 = -{&0i8};
    let _: i8 = !{&0i8};
}

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. Currently if !{ return } {} types successfully, since the block is checked against an expected type of bool. 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 with match !{ return } {}, which also fails currently, for the same reason. if !(return) {} and match !(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 of return on its own is !, which has a Not impl.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jan 23, 2026
Comment on lines -455 to +466
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines -244 to +247
if! {
if! (
match! (
break! {
return! {
break! (
return! (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dianne dianne added needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. needs-crater This change needs a crater run to check for possible breakage in the ecosystem. T-types Relevant to the types team, which will review and decide on the PR/issue. labels Jan 23, 2026
@dianne
Copy link
Contributor Author

dianne commented Jan 23, 2026

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 CoerceMany weirdness is unfortunate. That could maybe be minimized by changing how CoerceMany computes LUBs where all input types are !, but I'm not deeply familiar enough with coercions and inference to say whether that'd be okay (but I think I know how I'd implement it, at least, if that'd help in evaluating it).

But also it's quite possible this doesn't break any code in practice. !{ return } and -{ break } at least feel pretty unlikely to be real.

Anyway, I'm not sure what the process is for type system changes, so let's start with r? types

@dianne dianne marked this pull request as ready for review January 23, 2026 14:04
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jan 23, 2026
@fmease fmease changed the title HIR typeck: use correct expecatations for negation operators HIR typeck: use correct expectations for negation operators Jan 24, 2026
// 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 } {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor Author

@dianne dianne Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

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 of 1 gets 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).

Copy link
Contributor Author

@dianne dianne Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An attempt at clarifying the comments to be more specific about how and why it's helping inference: (diff)

@BoxyUwU
Copy link
Member

BoxyUwU commented Feb 4, 2026

Process wise this would just be a crater run + T-types FCP

@dianne dianne force-pushed the unop-expectations branch from d6fdfca to 3933e26 Compare February 4, 2026 21:36
@BoxyUwU
Copy link
Member

BoxyUwU commented Feb 14, 2026

The next step here would be the aformentioned crater run

@rustbot author

@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Feb 14, 2026
@rustbot
Copy link
Collaborator

rustbot commented Feb 14, 2026

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@rustbot rustbot added the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Feb 14, 2026
@dianne
Copy link
Contributor Author

dianne commented Feb 14, 2026

@bors try

@rust-bors

This comment has been minimized.

rust-bors bot pushed a commit that referenced this pull request Feb 14, 2026
HIR typeck: use correct expectations for negation operators
@rust-bors
Copy link
Contributor

rust-bors bot commented Feb 15, 2026

☀️ Try build successful (CI)
Build commit: 70daa2c (70daa2c0d8eed7066b07966f3c0b0b8622bff4e4, parent: a33907a7a5381473eec8bcfa0c56e05a856a911c)

@dianne
Copy link
Contributor Author

dianne commented Feb 15, 2026

@craterbot check

@craterbot
Copy link
Collaborator

👌 Experiment pr-151539 created and queued.
🤖 Automatically detected try build 70daa2c
🔍 You can check out the queue and this experiment's details.

ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

@craterbot craterbot removed the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Feb 15, 2026
@craterbot craterbot added the S-waiting-on-crater Status: Waiting on a crater run to be completed. label Feb 15, 2026
@craterbot
Copy link
Collaborator

🚧 Experiment pr-151539 is now running

ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

@dianne
Copy link
Contributor Author

dianne commented Feb 17, 2026

Apparently the bug this fixes was also reported as #114380. I've updated the description to close it too.

@craterbot
Copy link
Collaborator

🎉 Experiment pr-151539 is completed!
📊 10939 regressed and 2 fixed (816567 total)
📊 2619 spurious results on the retry-regressed-list.txt, consider a retry1 if this is a significant amount.
📰 Open the summary report.

⚠️ If you notice any spurious failure please add them to the denylist!
ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

Footnotes

  1. re-run the experiment with crates=https://crater-reports.s3.amazonaws.com/pr-151539/retry-regressed-list.txt

@craterbot craterbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-crater Status: Waiting on a crater run to be completed. labels Feb 18, 2026
@dianne
Copy link
Contributor Author

dianne commented Feb 18, 2026

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 -x.parse()?, where the result type of the parse call is only inferred because the match from ? introduces a coercion to the result type of the negation. let _: u8 = -x.parse().unwrap(); already doesn't compile, since there's not a match under the negation.

There's also at least one !unsafe { mem::transmute(0u8) }; there, it's the unsafe block that forces the result type of the transmute to coerce to the result type of the negation. let _: u8 = unsafe { !mem::transmute(0u8) }; already doesn't compile, since the block isn't under the negation.

@dianne
Copy link
Contributor Author

dianne commented Feb 18, 2026

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-crater This change needs a crater run to check for possible breakage in the ecosystem. needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unary operator applied to block fails to compile with type mismatch <T as UnaryOp>::Output != T overloaded unary "-" operator in match arms bug

4 participants