Skip to content
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

Document layout SemVer compatibility. #12169

Merged
merged 10 commits into from
Aug 18, 2023
Merged

Document layout SemVer compatibility. #12169

merged 10 commits into from
Aug 18, 2023

Conversation

ehuss
Copy link
Contributor

@ehuss ehuss commented May 23, 2023

This adds some documentation about whether or not alignment, layout, or size changes are SemVer-compatible.

@rustbot rustbot added A-documenting-cargo-itself Area: Cargo's documentation S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels May 23, 2023

It is a breaking change to change the alignment, layout, or size of a type that was previously well-defined.

In general, nominal types that use the [the default representation] do not have a well-defined alignment, layout, or size.
Copy link
Member

Choose a reason for hiding this comment

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

What distinction is "nominal types" meant to make, here? Unnamed types, like tuples, that use the default representation (which is the only option we provide for them as far as I know) also don't have a well-defined alignment, layout, or size.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was mostly trying to emphasize that this discussion was around types that accept the #[repr] attribute. However, it is not important and I dropped the word.


Some examples of changes that are not a breaking change are (assuming no other rules in this guide are violated):

* Adding, removing, or changing fields of a default representation struct, union, or enum.
Copy link
Member

Choose a reason for hiding this comment

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

This one is often going to be a breaking change, and while "assuming no other rules" above does cover that, leading with this one still seems potentially misleading. Should this say something along the lines of "assuming the changes do not otherwise break code using the types, such as code initializing an object of the type".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, yea, I was reluctant to try to repeat all the caveats, and defer to the comment above about assuming no other rules are broken. But you are correct that reading this on its own is very misleading. I have added some caveats, with links to the sections that explain them in more detail.

Some examples of changes that are not a breaking change are (assuming no other rules in this guide are violated):

* Adding, removing, or changing fields of a default representation struct, union, or enum.
* Adding variants to a default representation enum.
Copy link
Member

Choose a reason for hiding this comment

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

Likewise, this one is often going to be breaking, in the absence of non_exhaustive or an existing private variant. I don't want us to redundantly cover potentially breaking changes repeatedly, but I feel like this could use a bit of qualification (e.g. "Adding variants to a default representation enum, if the enum already has private variants or uses non_exhaustive")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added qualification here, too, with links.

Note that this may be a breaking change since it may change the size and alignment of the type.
Care should be taken in this case.
Public fields may be added if there are private fields, or it is `non_exhaustive`, and the addition does not alter the layout of the other fields.
* Adding variants to a `repr(C)` enum.
Copy link
Member

Choose a reason for hiding this comment

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

This is breaking if not non_exhaustive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same, added the caveat.

* Adding, removing, or changing private fields of a `repr(C)` struct, union, or enum.
Note that this may be a breaking change since it may change the size and alignment of the type.
Care should be taken in this case.
Public fields may be added if there are private fields, or it is `non_exhaustive`, and the addition does not alter the layout of the other fields.
Copy link
Member

Choose a reason for hiding this comment

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

Private fields are also breaking if there aren't yet any private fields and the type isn't non_exhaustive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same, added some caveats.

@rustbot rustbot added the A-semver Area: semver specifications, version matching, etc. label Jun 15, 2023
@ehuss
Copy link
Contributor Author

ehuss commented Jun 15, 2023

Thanks for the review!

I decided to go the route of splitting each point into a separate section. The #type-layout section provides an overview of all the rules, with links to the specific section. Each section can now go into more detail, and also shows examples (which highlights some of the more esoteric behaviors).

I was wondering if you had any comments on the two TODO comments. I feel like this is maybe going too far into the minutia, so I'm fine ignoring them for now. Sometimes people will just need to use their own judgement.

@ehuss ehuss marked this pull request as ready for review June 15, 2023 22:15
@ehuss
Copy link
Contributor Author

ehuss commented Jul 5, 2023

@joshtriplett Just checking in to see if you'll be able to review the updates.

In these cases, it may be safe to make changes to the types, though care should be exercised.
For example, types with private fields that do not otherwise document their alignment, layout, or size guarantees cannot be relied upon by external crates since the public API does not fully define the alignment, layout, or size of the type.

A common example where a type with *private* fields is well-defined is a type with a single private field with a generic type, using `repr(transparent)`, and which is documented as being transparent to the generic type.
Copy link
Member

Choose a reason for hiding this comment

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

"documented as being transparent" is ambiguous since rustdoc shows #[repr(transparent)] on some types even when they have private fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for taking a look! My intent wasn't that #[repr(transparent)] alone in the documentation doesn't mean it is "documented". I was pointing towards the actual text of the documentation discussing the guarantees it provides (for example, the linked UnsafeCell has an entire section about memory layout). If there is a private field, rustdoc by itself doesn't provide enough information to know what type the field is, and thus there is no way to know what the layout of the type is. The prose needs to explicitly indicate that the private field is just T, and that it is transparent to it (or some other explicit guarantee). I adjusted the text a little to try to make that clearer.

@bors
Copy link
Contributor

bors commented Jul 12, 2023

☔ The latest upstream changes (presumably #12339) made this pull request unmergeable. Please resolve the merge conflicts.

Copy link
Member

@obi1kenobi obi1kenobi left a comment

Choose a reason for hiding this comment

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

This is great, I'm strongly in support and I learned a lot from reading it. Thanks for putting it together!

I look forward to updating our existing cargo-semver-checks lints to point to the new sections, as well as adding new lints for things we don't already check.

If you have a moment to spare, I'd love your guidance on a few edge cases related to programmatically determining whether alignment changes are breaking or not — I put details inside PR comments in the relevant spots.

Comment on lines 637 to 630
#### Major: Changing the value N of `repr(packed(N))` if that changes the alignment or layout

It is a breaking change to change the value of N of `repr(packed(N))` if that changes the alignment or layout.
Copy link
Member

Choose a reason for hiding this comment

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

Tangent: might be nice to expand the repr(packed) nomicon docs here to mention that repr(packed) can take an argument. This is the first time I learned that's the case, and I've probably done a bit more digging through the reprs docs than average Rustaceans.

The Type Layout page's section on alignment modifiers of the reference also mentions it, but that's not the natural first place to go look up how repr(packed) can be used.

Comment on lines +642 to +635
If the value `N` is lowered below the alignment of a public field, then that would break any code that attempts to take a reference of that field.

Note that some changes to `N` may not change the alignment or layout, for example increasing it when the current value is already equal to the natural alignment of the type.
Copy link
Member

Choose a reason for hiding this comment

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

Tangent: any recommendations on how to write a high-quality, precise lint for this in cargo-semver-checks? Would love to lint this in a way that doesn't produce false-positives, but I don't feel I understand the nuances yet.

For example, if repr(packed(N)) is used on a type containing repr(Rust) values, how can we best (ideally, programmatically) figure out whether we've hit the "N lowered below alignment of public field" criterion if said fields are repr(Rust)? I'm confused because based on these additions I think the repr(Rust) values might not have well-defined layout or alignment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Computing the alignment and layout is a nontrivial thing to do. It is target-specific, and I think it would be a huge effort to duplicate everything the compiler does. Some things, like ctest2 create a Rust file with the structure and some assertions to check alignment and layout. The memoffset crate and the standard library functions like align_of_val can be used for gaining some information about a structure. -Zprint-type-sizes can be used to print the layout information (but only for human-readable debugging).

Overall, I don't think it is feasible for cargo-semver-checks to compute the actual layout and alignment.

For this specific clause, I probably wouldn't have any automated checks to determine if alignment changes. I would just say "any change to N is breaking".

You are correct that repr(Rust) does not have a well-defined layout or alignment. That means anything that contains those repr(Rust) types also does not have a well-defined layout.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, that's super useful and makes sense.

You are correct that repr(Rust) does not have a well-defined layout or alignment. That means anything that contains those repr(Rust) types also does not have a well-defined layout.

Love this! In fact, I think having that statement ("anything that contains repr(Rust) types does not have a well-defined layout") in the doc as an additional clarification might be valuable from a pedagogical point of view. Just my 2 cents.

src/doc/src/reference/semver.md Outdated Show resolved Hide resolved
src/doc/src/reference/semver.md Outdated Show resolved Hide resolved
@ehuss
Copy link
Contributor Author

ehuss commented Aug 6, 2023

Thanks for taking a look @obi1kenobi!

src/doc/src/reference/semver.md Outdated Show resolved Hide resolved
src/doc/src/reference/semver.md Outdated Show resolved Hide resolved
src/doc/src/reference/semver.md Outdated Show resolved Hide resolved
Comment on lines 228 to 229
* Adding variants to a `repr(C)` enum, if the enum uses `non_exhastive`.
See [repr-c-enum-variant-new](#repr-c-enum-variant-new).
Copy link
Member

Choose a reason for hiding this comment

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

The reordering concern I mentioned above seems to pose an interesting question here as well: if the new variants of the #[repr(C)] #[non_exhaustive] enum are added in the middle, they may change the (externally-observable) discriminants of existing variants. Might that be a major change?

Adding new variants after all existing variants seems safe, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think whether or not changing enum discriminant values is specific to the situation. I see that as a similar situation to whether or not changing a const is a breaking change. I think in most cases the intent is to have some abstract representation of some value, but the exact value of the const (or enum discriminant) doesn't matter. https://internals.rust-lang.org/t/should-we-be-more-strict-with-const-and-semver/14302 contains more discussion on this. I'm not sure what the rule should be for that, but I added it to #8736 to track it.

Comment on lines +255 to +264
* Removing `repr(C)` from a struct, union, or enum.
See [repr-c-remove](#repr-c-remove).
Copy link
Member

Choose a reason for hiding this comment

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

Is there broad consensus on repr being always public, or can it sometimes be private?

A few folks involved with the Rust project recently mentioned that in their view, it's possible for repr(C) and other repr attributes to be public, and that a removal of repr(C) is not necessarily a semver-major change.

This is the PR in question where that discussion was taking place: rust-lang/rust#90435

Copy link
Member

Choose a reason for hiding this comment

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

I think I misunderstood something, we're good.

Comment on lines +243 to +252
* Adding `repr(align)` to a struct, union, or enum.
See [repr-align-add](#repr-align-add).
Copy link
Member

Choose a reason for hiding this comment

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

This is true, but given how rare and suboptimal repr(packed) is, I suspect in practice some crates might not treat this as a breaking change.

Also, it seems like in theory we could remove the incompatibility between the two, though I doubt that's a priority.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, there's a TODO comment in that section about how unlikely that is to be a problem. I'm not sure how best to handle cases like that. There are more shades than I expected in the spectrum from "definitely compatible" to "definitely not compatible", and it is hard to know how much of a stickler to be.

I think this is something to revisit and I'm going to leave the TODO in there for now.

Comment on lines +253 to +262
* Changing the order of public fields of a `repr(C)` type.
See [repr-c-shuffle](#repr-c-shuffle).
Copy link
Member

Choose a reason for hiding this comment

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

It seems like, as described, this is only a breaking change if the type has no private fields? Or should we consider the offsets of public fields to be stable even if there are private fields?

Copy link
Member

Choose a reason for hiding this comment

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

Nevermind, the disclaimer at the end of this list saying "In some cases, types with a repr attribute may not have an alignment, layout, or size that is well-defined." covers this well enough. (Might be worth mentioning that in the specific section, but not blocking on this.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I rearranged the text to put the caveat higher. It can be challenging to be precise, but keep things concise enough to read without repeating all the qualifications.

@joshtriplett
Copy link
Member

r=me with the (very minor) wording nits fixed.

@ehuss
Copy link
Contributor Author

ehuss commented Aug 18, 2023

Thanks for the review!
@bors r=joshtriplett

@bors
Copy link
Contributor

bors commented Aug 18, 2023

📌 Commit 5dc86dc has been approved by joshtriplett

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 18, 2023
@bors
Copy link
Contributor

bors commented Aug 18, 2023

⌛ Testing commit 5dc86dc with merge 5633e2a...

@bors
Copy link
Contributor

bors commented Aug 18, 2023

☀️ Test successful - checks-actions
Approved by: joshtriplett
Pushing 5633e2a to master...

@bors bors merged commit 5633e2a into rust-lang:master Aug 18, 2023
18 checks passed
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Aug 19, 2023
Update cargo

15 commits in 7c3904d6c3ed54e8a413023519b55a536ad44d5b..80eca0e58fb2ff52c1e94fc191b55b37ed73e0e4
2023-08-14 20:11:43 +0000 to 2023-08-19 00:52:06 +0000
- chore: Downgrade serde below the binary blob (rust-lang/cargo#12528)
- Improve error message for when no credential providers are available (rust-lang/cargo#12526)
- Fix typo: "use" -> "used" (rust-lang/cargo#12522)
- Document layout SemVer compatibility. (rust-lang/cargo#12169)
- Make cargo-credential-gnome-secret built-in as cargo:libsecret (rust-lang/cargo#12521)
- login: allow passing additional args to provider (rust-lang/cargo#12499)
- cargo-credential-gnome-secret: dynamically load libsecret (rust-lang/cargo#12518)
- credential-providers: make 1password no longer built-in (rust-lang/cargo#12507)
- Print environment variables for `cargo run` in extra verbose mode (rust-lang/cargo#12498)
- chore(cargo-util): bump version to 0.2.6 (rust-lang/cargo#12517)
- credential: rename cargo:basic to cargo:token-from-stdout (rust-lang/cargo#12512)
- fix(xtask-bump-check): query by package name to detect changes (rust-lang/cargo#12513)
- ci: use pull request head commit whenever possible (rust-lang/cargo#12508)
- Update hermit-abi (rust-lang/cargo#12504)
- Crate checksum lookup query should match on semver build metadata (rust-lang/cargo#11447)

r? ghost
bors added a commit to rust-lang-ci/rust that referenced this pull request Aug 19, 2023
Update cargo

15 commits in 7c3904d6c3ed54e8a413023519b55a536ad44d5b..80eca0e58fb2ff52c1e94fc191b55b37ed73e0e4
2023-08-14 20:11:43 +0000 to 2023-08-19 00:52:06 +0000
- chore: Downgrade serde below the binary blob (rust-lang/cargo#12528)
- Improve error message for when no credential providers are available (rust-lang/cargo#12526)
- Fix typo: "use" -> "used" (rust-lang/cargo#12522)
- Document layout SemVer compatibility. (rust-lang/cargo#12169)
- Make cargo-credential-gnome-secret built-in as cargo:libsecret (rust-lang/cargo#12521)
- login: allow passing additional args to provider (rust-lang/cargo#12499)
- cargo-credential-gnome-secret: dynamically load libsecret (rust-lang/cargo#12518)
- credential-providers: make 1password no longer built-in (rust-lang/cargo#12507)
- Print environment variables for `cargo run` in extra verbose mode (rust-lang/cargo#12498)
- chore(cargo-util): bump version to 0.2.6 (rust-lang/cargo#12517)
- credential: rename cargo:basic to cargo:token-from-stdout (rust-lang/cargo#12512)
- fix(xtask-bump-check): query by package name to detect changes (rust-lang/cargo#12513)
- ci: use pull request head commit whenever possible (rust-lang/cargo#12508)
- Update hermit-abi (rust-lang/cargo#12504)
- Crate checksum lookup query should match on semver build metadata (rust-lang/cargo#11447)

r? ghost
@ehuss ehuss added this to the 1.73.0 milestone Aug 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-documenting-cargo-itself Area: Cargo's documentation A-semver Area: semver specifications, version matching, etc. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants