Skip to content

Add internal #[rustc_assert_variance] attribute for use in std - #163578

Open
RuleOfSix wants to merge 8 commits into
rust-lang:mainfrom
RuleOfSix:rustc-assert-variance-attr
Open

RuleOfSix wants to merge 8 commits into
rust-lang:mainfrom
RuleOfSix:rustc-assert-variance-attr

Conversation

@RuleOfSix

@RuleOfSix RuleOfSix commented Oct 1, 2026 •

Copy link
Copy Markdown

View all comments

Add a new attribute #[rustc_assert_variance] to ensure the variance of a generic parameter of an algebraic data type at compile time.

The attribute attaches directly to generic parameters of an ADT declaration, and will cause a compile error if the variance of the type does not match what is stated. For example:

struct Foo<#[rustc_assert_variance(covariant)] 'a, #[rustc_assert_variance(covariant)] T> {
    val: &'a mut T,
}

Trying to compile this struct definition gives this error:

error: `Foo` is expected to be covariant in `T` but is not
 --> src/main.rs:3:1
  |
3 | pub struct Foo<#[rustc_assert_variance(covariant)] 'a, #[rustc_assert_variance(covariant)] T> {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-^
  |                                                                                            |
  |                                                                                            `Foo` is invariant in `T`
  |
note: required by this attribute
 --> src/main.rs:3:56
  |
3 | pub struct Foo<#[rustc_assert_variance(covariant)] 'a, #[rustc_assert_variance(covariant)] T> {
  |                                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The goal is for this to help prevent regression in the variance of standard library types leading to accidental breaking changes in the API. This PR adds this attribute to the definitions of many std wrapper types to this end.

Closes #163513

r? mejrs

@rustbot

rustbot commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Some changes occurred in compiler/rustc_attr_ir

cc @jdonszelmann, @JonathanBrouwer

Some changes occurred in compiler/rustc_attr_parsing

cc @jdonszelmann, @JonathanBrouwer

Some changes occurred in compiler/rustc_passes/src/check_attr.rs

cc @jdonszelmann, @JonathanBrouwer

@rustbot rustbot added A-attributes Area: Attributes (`#[…]`, `#![…]`) 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. labels Oct 1, 2026
@rustbot

rustbot commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the pull request, and welcome! The Rust Project has assigned @mejrs (or someone else) to review your changes, you should hear from them (or someone else) within the next two weeks.

Please see the contribution instructions and our LLM policy for more information.

@mejrs mejrs left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To improve readability, I decided to have the attribute attach to the ADT rather than to a particular parameter

On the other hand, this choice complicates the syntax (in a different way) and, as you say, introduces some ambiguity. It also means we have to check whether the given parameter really is a generic parameter of the type. I strongly prefer to have it on the generic parameter instead.

View changes since this review

Comment thread tests/ui/attributes/rustc_assert_variance.rs
Comment thread compiler/rustc_attr_parsing/src/attributes/rustc_internal.rs Outdated
Comment thread compiler/rustc_passes/src/diagnostics.rs Outdated
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Oct 1, 2026
@rustbot

rustbot commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

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

}
}

fn check_rustc_assert_variance(

@fmease fmease Oct 1, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In PR #161351 I moved a #[rustc_dump*] impl out of check_attr because I believe this module is meant to only contain complex target & validity checks for attributes, not however their "business logic".

I guess rustc_hir_analysis would be a good fit here, too?

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see no harm in it being in check_attr, stuff like "is #[may_dangle] on a Drop impl" is also in check_attr. Happy to cede to wherever you think this fits best though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, I just thought y'all wanted to yeet check_attr.rs at some point & have parsing & validity checks in rustc_attr_parsing in which case it would be better not to add onto it but I don't know if that's still the plan / even feasible.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's a decent amount of "need to run queries etc for this check" code in check_attr, so getting rid of check_attr entirely isn't going to happen sadly.

@RuleOfSix

Copy link
Copy Markdown
Author

@rustbot ready

Following mejrs' feedback, I changed the attribute to be attached to the generic parameters instead of the ADT. This simplified a lot of the implementation (AttributeKind::RustcAssertVariance no longer has to be boxed due to size concerns for instance).

I updated the tests to match, added support + testing for bivariance asserts, added a test for const generics, and fixed the diagnostic message. I left the variance check in check_attrs.rs for now, as it is arguably just a complex test of valid targets, but will happily move it if needed.

@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 Oct 1, 2026

@mejrs mejrs left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's a little confusing to try to tell what's wrong by just looking at the diagnostics. Because the attribute is long, many structs will get formatted like

struct Foo6<
    #[rustc_assert_variance(contravariant)]
    //~^ NOTE required by this annotation
    'a,
}

so the error messages will look like this:

error: `Foo6` is covariant in `'a`; expected variance: contravariant
  --> $DIR/rustc_assert_variance.rs:44:5
   |
LL |     'a,
   |     ^^
   |

and just by glancing at this it's hard to tell what's happening. And someone, somewhere, is going to have to look at this when it's in a CI error log or lazily pasted into an issue or whatever.

Can you use the struct definition span for the primary span and the parameter span for a label?

Then the error message should look something like this:

error: `Foo6` is expected to be contravariant in `'a` but it is not
  --> $DIR/rustc_assert_variance.rs:44:5
/ struct Foo6<
|    #[rustc_assert_variance(contravariant)]
|    'a,
| }  ^^ `Foo6` is covariant in `'a`
|-^

(possibly with the note pointing into that as well)

View changes since this review

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Oct 2, 2026
@mejrs

mejrs commented Oct 2, 2026

Copy link
Copy Markdown
Member

Other than the error message spans it looks good.

Please apply it to a bunch of std types next. I'm thinking of the "phantom family", the "cell" types (Cell/UnsafeCell/etc) and the lock and guard types?

Then can you figure out who actually asked for this (I'm not sure, someone on the libs team?) and ask them to take a look and whether they're happy with it?

…the size of `AttributeKind`

- Add ui test for #[rustc_assert_variance]
…rameters instead of their parent ADTs

- Add support for bivariance to rustc_assert_variance
- Update rustc_assert_variance ui test accordingly and add case for const generic params
- Minor diagnostic message fix
@RuleOfSix
RuleOfSix force-pushed the rustc-assert-variance-attr branch from 9cc4d54 to 78f106f Compare October 3, 2026 04:54
@rustbot

rustbot commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@RuleOfSix

RuleOfSix commented Oct 3, 2026 •

Copy link
Copy Markdown
Author

@Mark-Simulacrum I think you were the one who initially suggested something like this in the discussion on #159838 (comment). Does this seem like a good solution to assert variance properties of public APIs in std, or do you think something is still missing here?

@rust-log-analyzer

This comment has been minimized.

@mejrs

mejrs commented Oct 3, 2026

Copy link
Copy Markdown
Member

Please also update the pr description, its outdated.

…r types, cell types, sync lock and guard types, Arc, Rc, Unique, Box, and NonNull
@RuleOfSix
RuleOfSix force-pushed the rustc-assert-variance-attr branch from 78f106f to 08c336c Compare October 3, 2026 15:26
@RuleOfSix

Copy link
Copy Markdown
Author

Please also update the pr description, its outdated.

Done. I also fixed a ui test failure that was happening because of the definition of MutexGuard spilling onto multiple lines with the attribute applied.

@mejrs

mejrs commented Oct 3, 2026

Copy link
Copy Markdown
Member

@Mark-Simulacrum I think you were the one who initially suggested something like this in the discussion on #159838 (comment). Does this seem like a good solution to assert variance properties of public APIs in std, or do you think something is still missing here?

cc @WaffleLapkin @clarfonthey as well

Comment on lines +724 to +727
Covariant,
Invariant,
Contravariant,
Bivariant,

@clarfonthey clarfonthey Oct 3, 2026 •

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.

I know it technically doesn't matter for the implementation, but I do kinda wish these were:

Suggested change
Covariant,
Invariant,
Contravariant,
Bivariant,
Invariant = 0b00,
Covariant = 0b01,
Contravariant = 0b10,
Bivariant = 0b11,

so that they internally represent the variance a bit better, even if we don't rely on that.

View changes since the review

@RuleOfSix RuleOfSix Oct 3, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I like this idea, but it seems more relevant for rustc_type_ir::Variance since that's what's actually used by the compiler to represent variance.

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.

(also worth asking whether these two types should be different at all)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would rather not that rustc_attr_ir depends on rustc_type_ir, or the other way around.

It could live earlier in the crate graph, in rustc_structures, but moving it would also mean moving a lot of Variance related methods out of rustc_type_ir. So merging these two comes with downsides.

const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[
Allow(Target::TypeParam),
Allow(Target::LifetimeParam),
Allow(Target::ConstParam),

@clarfonthey clarfonthey Oct 3, 2026 •

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.

Can const parameters be anything other than invariant? 😨

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not to my knowledge. It's more there for syntactic completeness so that the attribute can attach to any generic parameter.

@clarfonthey

Copy link
Copy Markdown
Contributor

Side note worth mentioning: there currently isn't a test here for something that has an actually bivariant lifetime, and that probably should be added.

@RuleOfSix

RuleOfSix commented Oct 3, 2026 •

Copy link
Copy Markdown
Author

Side note worth mentioning: there currently isn't a test here for something that has an actually bivariant lifetime, and that probably should be added.

I'm struggling to come up with an example where this is possible for an ADT (as opposed to a function). You can't do something like a struct that only stores a T with a T: SomeTrait<'a> bound because the compiler will complain that you're not using the lifetime, but adding any kind of phantom data so that you are using the lifetime inherently prevents bivariance.

I'm not super familiar with the intricacies of variance semantics though, so if you know how this could be done I'd love to add a case like that for sure.

@clarfonthey

Copy link
Copy Markdown
Contributor

So, was looking at this myself, and here's a great example of a bivariant lifetime:

struct Test<'a>;

🤦🏻

Yes, I had forgotten about this case, but it's a pretty easy one to cover.

@fmease

fmease commented Oct 3, 2026

Copy link
Copy Markdown
Member

I already gave a positive example for bivariant parameters here #163578 (comment) but I guess y'all couldn't find it because it's marked as resolved.

As I said, bivariant parameters are legal if they're constrained.

@RuleOfSix

Copy link
Copy Markdown
Author

I already gave a positive example for bivariant parameters here #163578 (comment) but I guess y'all couldn't find it because it's marked as resolved.

As I said, bivariant parameters are legal if they're constrained.

That case is in the tests, we were discussing bivariant lifetime parameters specifically.

So, was looking at this myself, and here's a great example of a bivariant lifetime:

struct Test<'a>;

🤦🏻

Yes, I had forgotten about this case, but it's a pretty easy one to cover.

The issue with this is that when I try to compile this with the attribute, the 'lifetime not used' error aborts compilation before the attribute is checked, so there's no error from the attribute to test:

error[E0392]: lifetime parameter `'a` is never used
 --> src/main.rs:4:49
  |
4 | struct Test<#[rustc_assert_variance(bivariant)] 'a>;
  |                                                 ^^ unused lifetime parameter
  |
  = help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData`

@clarfonthey

Copy link
Copy Markdown
Contributor

You could just allow the lint and then it should work fine.

@fmease

fmease commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

The very same thing applies: You just need to constrain the bivariant generic parameter in question:

struct X</*bi*/ 'a, /*co*/ T>(T) where T: Iterator<Item = &'a ()>;

@RuleOfSix

Copy link
Copy Markdown
Author

You could just allow the lint and then it should work fine.

I believe unused generic parameters are a hard error in the compiler, not a lint.

The very same thing applies: You just need to constrain the bivariant generic parameter in question:

struct X</*bi*/ 'a, /*co*/ T>(T) where T: Iterator<Item = &'a ()>;

🤦‍♀️ this is so obvious in retrospect. Test case added, thank you for the help.

@clarfonthey

Copy link
Copy Markdown
Contributor

I believe unused generic parameters are a hard error in the compiler, not a lint.

Parameters yes, lifetime parameters no, unfortunately.

@RuleOfSix

RuleOfSix commented Oct 3, 2026 •

Copy link
Copy Markdown
Author

I believe unused generic parameters are a hard error in the compiler, not a lint.

Parameters yes, lifetime parameters no, unfortunately.

Really? I can't find what the lint to disable would even be, and the Rust Reference seems to say otherwise:

Unlike type and lifetime parameters, const parameters can be declared without being used inside of a parameterized item, with the exception of implementations as described in generic implementations:

// ok
struct Foo<const N: usize>;
enum Bar<const M: usize> { A, B }

// ERROR: unused parameter
struct Baz<T>;
struct Biz<'a>;
struct Unconstrained;
impl<const N: usize> Unconstrained {}

@rust-log-analyzer

This comment has been minimized.

@clarfonthey

Copy link
Copy Markdown
Contributor

Really? I can't find what the lint to disable would even be, and the Rust Reference seems to say otherwise:

Right, I actually completely misremembered this; you can have unconstrained lifetime parameters in impls, but not structs.

@RuleOfSix
RuleOfSix force-pushed the rustc-assert-variance-attr branch from 6ced8bb to 7cf50bf Compare October 3, 2026 22:31

This branch has not been deployed

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

Labels

A-attributes Area: Attributes (`#[…]`, `#![…]`) 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a way to assert variance, to be used in std

6 participants