Skip to content

fix: emit E0600 when unary !/- is applied to unsupported type - #23147

Merged
ChayimFriedman2 merged 1 commit into
rust-lang:masterfrom
kivancgnlp:e0600-unary-op-not-defined
Aug 17, 2026
Merged

fix: emit E0600 when unary !/- is applied to unsupported type#23147
ChayimFriedman2 merged 1 commit into
rust-lang:masterfrom
kivancgnlp:e0600-unary-op-not-defined

Conversation

@kivancgnlp

@kivancgnlp kivancgnlp commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Addresses the FIXME in infer_user_unop, which previously discarded operator method resolution failures for !x and -x expressions.

When the operand's type does not implement std::ops::Not (for !) or std::ops::Neg (for -), rust-analyzer now reports the same E0600 error that rustc produces:

cannot apply unary operator `!` to type `Question`

Wired through the standard inference diagnostic pipeline: new InferenceDiagnostic::UnaryOperatorCannotBeApplied variant in hir-ty, matching UnaryOperatorCannotBeApplied struct plus conversion in hir, and a handler in ide-diagnostics using DiagnosticCode::RustcHardError("E0600").

The diagnostic is suppressed when the operand is an unresolved type variable or already references an error, so that incomplete code and macro expansions that infer to {unknown} do not produce spurious E0600 reports.

The unary_ops region of crates/test-utils/src/minicore.rs also gains builtin Not and Neg impls, mirroring how add_impl! provides them in the add region. Without these, the diagnostic test fixture would incorrectly flag !true, !0i32 and similar builtin uses as errors, because lookup_op_method would find no impl in the minicore fixture even though real core has one. With the impls present, primitives resolve normally and only genuinely unsupported operators trigger the diagnostic. This also lets us correctly report -1u32 as E0600, since real core does not implement Neg for unsigned integers.

Because the new not_impl! / neg_impl! blocks live in a nested region:builtin_impls inside region:unary_ops, the new tests opt into both flags via //- minicore: unary_ops, builtin_impls. The existing legacy_const_generics test in mismatched_arg_count uses -1i32 / -1i8 inline and now needs the same directive so that core::ops::Neg is in scope for its operands.

The UnaryOp::Deref case is left unchanged; it is already handled by the CannotBeDereferenced diagnostic (E0614) and infer_user_unop is never called for Deref.

Refs #22140

r? @ChayimFriedman2

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 15, 2026
@kivancgnlp
kivancgnlp marked this pull request as draft August 15, 2026 05:09
@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 15, 2026
@kivancgnlp
kivancgnlp force-pushed the e0600-unary-op-not-defined branch from 681dfbb to 3c8ef6d Compare August 15, 2026 05:21
@kivancgnlp

Copy link
Copy Markdown
Contributor Author

@ChayimFriedman2 while I was thoroughly testing the alternative solutions I hit a subtle interaction and wanted to check with you before pushing.

The problem

minicore_smoke_test iterates every flag individually and checks each stripped source is diagnostic-clean. Several regions contain builtin unary ops:

  • region:eq: !self.eq(other) in PartialEq::ne, Less = -1 on Ordering
  • region:float_consts: NEG_INFINITY: f32 = -0.0 in four places

When those regions are active without unary_ops, lookup_op_method bails with an empty error and the diagnostic fires on minicore's own source. I also hit two other cases:

  • handlers::mismatched_arg_count::tests::legacy_const_generics: -1i32 / -1i8 inline with no minicore
  • tests::overly_long_real_world_cases::tracing_infinite_repeat: macro expansion inferring to {unknown}

Fixture-only fix I tried

Rewriting !self.eq(other) as match self.eq(other) { true => false, false => true } fixed the eq case, but -0.0 in const context and -1 as an enum discriminant do not have clean non-unary rewrites. Making eq and float_consts depend on unary_ops would work but cuts against the point of granular flags.

What I have working

A guard inside infer_user_unop that suppresses the diagnostic when the operand:

  • is an unresolved type variable, or
  • already references an inference error, or
  • is a primitive the language always supports even when the trait is absent: ! on bool and any integer, - on signed integer, float, or unresolved numeric literal.

-1u32 still triggers E0600 because unsigned integers are excluded.

Test results

cargo test -p ide-diagnostics with RUN_SLOW_TESTS=1: all 720 tests pass, including minicore_smoke_test, legacy_const_generics, tracing_infinite_repeat, and the eight new unary_operator_cannot_be_applied tests.

Framing

Your suggestion was a fixture-only fix: add minicore directives and either implement primitive traits in minicore or switch tests to user types. I did the first half (directives + primitive impls in region:builtin_impls) and that clears the eight new tests, but the smoke test cascade above forced a wider fix, and pure fixture rewriting stops working at -0.0 in const context and -1 as an enum discriminant.

The guard I have handles cases the trait mechanism cannot decide because the trait itself is not in scope, which feels like a different problem from what your original guidance was ruling out (avoiding a caller-side workaround for missing primitive impls). Wanted to check that framing with you before pushing.

@ChayimFriedman2

Copy link
Copy Markdown
Contributor

Did you use AI for this? If yes, please read our AI policy. In particular, using AI to write GitHub comments, including PR descriptions, is forbidden.

@ChayimFriedman2 ChayimFriedman2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As for the minicore smoke test, do not change the non-test logic for them. I think making the relevant minicore flags (eq and float_consts) depend on unary_ops, builtin_impls will be the best fix.

View changes since this review

Comment thread crates/hir-ty/src/infer/op.rs Outdated
// contain an error do not represent a real user mistake
// here, and would otherwise fire on incomplete code and on
// macro expansions that resolve to `{unknown}`.
if !operand_ty.is_ty_var() && !operand_ty.references_error() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not the place. They should be filtered in resolve_diagnostics().

@kivancgnlp

Copy link
Copy Markdown
Contributor Author

Hi @ChayimFriedman2, yes, I used AI to help me understand the codebase more deeply and generate alternative solutions, which I understand is allowed by the policy. I reviewed and tested every change thoroughly on my Windows machine.

I've implemented the solution like you said. The diagnostic is now filtered in resolve_diagnostics, and the infer/op.rs guard is removed. region:eq and region:float_consts depend on unary_ops, builtin_impls in minicore.

One small note: builtin_impls itself also needs index and slice, because Clone for [T; 1] uses self[0]. All tests pass locally with RUN_SLOW_TESTS=1.

@kivancgnlp
kivancgnlp force-pushed the e0600-unary-op-not-defined branch from 9e331e2 to 55fe0b0 Compare August 16, 2026 06:43
@kivancgnlp
kivancgnlp marked this pull request as ready for review August 16, 2026 06:53
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 16, 2026

@ChayimFriedman2 ChayimFriedman2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please also squash.

View changes since this review

Comment thread crates/hir-ty/src/infer/op.rs Outdated
}
Err(_errors) => {
// FIXME: Report diagnostic.
// The diagnostic is filtered in `resolve_diagnostics` when the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment is redundant. this is true for all diagnostics containing types. Please remove it.

Comment thread crates/test-utils/src/minicore.rs Outdated
//! async_fn: fn, tuple, future, copy
//! bool_impl: option, fn
//! builtin_impls:
//! builtin_impls: index, slice

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

builtin_impls shouldn't depend on anything. If some code needs additional flags, it should also be guarded by those flags, e.g.:

// region:builtin_impls
// region:index
...
// endregion:index
// endregion:builtin_impls

Addresses the FIXME in `hir-ty/src/infer/op.rs` inside
`infer_user_unop`, which previously silently discarded operator method
resolution failures for `!x` and `-x` expressions.

When the operand's type does not implement `std::ops::Not` (for `!`) or
`std::ops::Neg` (for `-`), rust-analyzer now reports the same E0600
error that rustc produces:

    cannot apply unary operator `!` to type `Question`

Wired through the standard inference diagnostic pipeline: new
`InferenceDiagnostic::UnaryOperatorCannotBeApplied` variant in hir-ty,
matching `UnaryOperatorCannotBeApplied` struct plus conversion in hir,
and a handler in ide-diagnostics using
`DiagnosticCode::RustcHardError("E0600")`.

Filtering for unresolved / error-typed operands is done in
`resolve_diagnostics()` (crates/hir-ty/src/infer/unify.rs) alongside
the existing `references_non_lt_error()` filter chain for other
diagnostics that carry a type. This keeps `infer_user_unop` free of
callsite guards and lets the natural inference pipeline suppress
spurious reports on incomplete code and on macro expansions that
infer to `{unknown}`.

The `unary_ops` region of `test-utils/src/minicore.rs` also gains
builtin `Not` and `Neg` impls, mirroring how `add_impl!` provides them
in the `add` region. Without these, the diagnostic test fixture would
incorrectly flag `!true`, `!0i32` and similar builtin uses as errors,
because `lookup_op_method` would find no impl in the minicore fixture
even though real `core` has one. With the impls present, primitives
resolve normally and only genuinely unsupported operators trigger the
diagnostic. This also lets us correctly report `-1u32` as E0600, since
real `core` does not implement `Neg` for unsigned integers.

Because the new `not_impl!` / `neg_impl!` blocks live in a nested
`region:builtin_impls` inside `region:unary_ops`, the new tests opt
into both flags via `//- minicore: unary_ops, builtin_impls`. The
existing `legacy_const_generics` test in `mismatched_arg_count` uses
`-1i32` / `-1i8` inline and now needs the same directive so that
`core::ops::Neg` is in scope for its operands.

Minicore `region:eq` and `region:float_consts` now depend on
`unary_ops, builtin_impls` so their smoke tests resolve `Not`/`Neg`
without per-callsite guards. The `Clone for [T; 1]` impl inside
`region:builtin_impls` uses `self[0]`, so it is scoped to a nested
`region:index` and only compiles when `index` is also enabled.

The `UnaryOp::Deref` case is left unchanged; it is already handled by
the `CannotBeDereferenced` diagnostic (E0614) and `infer_user_unop` is
never called for `Deref`.

Part of #22140.
@kivancgnlp
kivancgnlp force-pushed the e0600-unary-op-not-defined branch from 55fe0b0 to e6f2eb5 Compare August 17, 2026 03:49
@kivancgnlp

Copy link
Copy Markdown
Contributor Author

Thanks, I've done the changes.

  • comment removed
  • builtin_impls deps reverted with the Clone for [T; 1] impl scoped to a nested region:index
  • squashed to a single commit
  • all tests green locally

@kivancgnlp

Copy link
Copy Markdown
Contributor Author

I noticed an error on the Windows CI runner. All tests ran successfully locally on my Windows computer. I believe a transient glitch caused CRYPT_E_REVOCATION_OFFLINE during the cargo-nextest install step.

@ChayimFriedman2 ChayimFriedman2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@ChayimFriedman2
ChayimFriedman2 added this pull request to the merge queue Aug 17, 2026
Merged via the queue into rust-lang:master with commit 4bd323d Aug 17, 2026
32 of 34 checks passed
@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants