Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,21 @@
- [Language](./lang/index.md)
- [RFC Merge Procedure](./lang/rfc-merge-procedure.md)
- [Stabilization procedure](./lang/stabilization-procedure.md)
- [Libs](./libs/index.md)
- [Maintaining the standard library](./libs/maintaining-std.md)
- [Library](./libs/index.md)
- [Maintaining APIs](./libs/apis/index.md)
- [API Change Proposals (ACPs)](./libs/apis/proposals.md)
- [Changing APIs](./libs/apis/changing.md)
- [API stabilization](./libs/apis/stabilization.md)
- [Fixing APIs](./libs/apis/fixing.md)
- [Maintaining Implementations](./libs/impls/index.md)
- [Code review](./libs/impls/review.md)
- [Crate maintenance](./libs/impls/crates.md)
- [Target tiers](./libs/impls/targets.md)
- [Testing and debugging](./libs/impls/testing-debugging.md)
- [Performance and benchmarking](./libs/impls/perf-benchmarking.md)
- [Meetings](./libs/meetings.md)
- [Membership](./libs/membership.md)
- [Repositories](./libs/repositories.md)
- [Release](./release/index.md)
- [Backporting](./release/backporting.md)
- [Preparing Release Notes](./release/release-notes.md)
Expand Down
159 changes: 159 additions & 0 deletions src/libs/apis/changing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Changing APIs

All new guarantees made by the standard library need [an FCP](../membership.md#fcp-process), but what constitutes a new guarantee or breaking change is extremely unclear. This list attempts to cover as many cases as possible that need stabilization based upon experience, although it definitely may be incomplete.

For changes which aren't of concern for API surface, see the section on [code review].

[code review]: ../impls/review.md

## API surface

Any new API added to the standard library needs an FCP. New types, new methods, new traits, and new trait implementations all represent changes that need an FCP to make. These will usually, but not always, involve changing an `#[unstable]` attribute to a `#[stable]` attribute, or a `#[rustc_const_unstable]` attribute to a `#[rustc_const_stable]` attribute. Note that stability attributes will also change from including a tracking issue to a Rust version, and the special `CURRENT_RUSTC_VERSION` string should be used for new stabilizations; these will be replaced with the correct version when the version is actually released.

## Implementation-derived guarantees

There are several ways that changing Rust code can unintentionally make new guarantees for an API. For example, changing trait bounds *usually* represents a new guarantee, although some bounds can never be changed due to them *removing* guarantees. Changing [restrictions] (unstable) can also affect guarantees.

[restrictions]: https://github.com/rust-lang/rust/issues/105077

Even outside trait bounds, changing the inner contents of a type may affect its [variance] in ways which are publicly noticeable, which are new guarantees. The contents of a type may also subtly affect its implementation of [auto traits] like [`Send`] and [`Sync`] which can cause publicly noticeable changes.

[variance]: https://doc.rust-lang.org/nightly/reference/subtyping.html#subtyping.variance
[auto traits]: https://doc.rust-lang.org/nightly/reference/special-types-and-traits.html#auto-traits
[`Send`]: https://doc.rust-lang.org/nightly/std/marker/trait.Send.html
[`Sync`]: https://doc.rust-lang.org/nightly/std/marker/trait.Sync.html

## Type inference

[RFC 1105] explicitly details what kinds of breakages are considered acceptable, and one of those breakages is adding new trait implementations. Unfortunately, things aren't that easy.

[RFC 1105]: https://rust-lang.github.io/rfcs/1105-api-evolution.html

Due to the way the type system works, type inference can break if new trait implementations are added. For example, imagine the following trait impl:

```rust
impl From<&str> for Arc<str> { /* ... */ }
```

With only this impl, the following code works and will correctly infer a conversion from `&str` to `Arc<str>`:

```rust
let b = Arc::from("a");
```

However, if we add a new impl:

```rust
impl From<&str> for Arc<[u8]> { /* ... */ }
```

All of a sudden, the code becomes ambiguous; the type of the result cannot be inferred from the method call. Technically, breakages like this are allowed by our guarantees, but if too many Rust users rely on it, we may decide to disallow them anyway. Similarly, generalizing methods with traits can *also* have this kind of issue, for example:

```rust
fn new<T>(a: &str) -> Arc<T>
where
Arc<T>: From<&str>
{ /* ... */ }
```

has the same issue. Sometimes, even adding *unstable* features can still result in inference failures.

While [crater] can be used to determine the exact impact of a change on the larger ecosystem, it is not bulletproof and the team may choose to be overly cautious when accepting changes.

## Deref coercion

In addition to type inference, deref coercion can also break depending on new implementations. For example, with just the following:

```rust
impl Deref for String { type Target = str; /* ... */ }
impl Add<&str> for String { /* ... */ }
```

The following code works, since `b` is deref-coerced from `&String` into `&str`:

```rust
let a = String::from("a");
let b = String::from("b");
let c = a + &b;
```

However, if we add a new impl:

```rust
impl Add<char> for String { /* ... */ }
```

Suddenly, Rust won't perform deref coercion and complain about `Add<&String>` missing instead. These types of cases are especially tricky to notice.

## Method resolution

New methods can sometimes affect code in unpredictable ways. For example, unstable methods added to `Iterator` with the same name as methods on other popular crates like [`itertools`] can cause unintended side effects. In general, these cases will trigger the [`unstable-name-collisions` lint], but libs can still be reluctant to make changes for extremely common method names. In the past, we've even made [dedicated compiler workarounds], [multiple times] to get around method resolution issues.

[`Iterator`]: https://doc.rust-lang.org/nightly/std/iter/trait.Iterator.html
[`itertools`]: https://docs.rs/itertools
[`unstable-name-collisions` lint]: https://doc.rust-lang.org/rustc/lints/listing/warn-by-default.html#unstable-name-collisions
[dedicated compiler workarounds]: https://doc.rust-lang.org/nightly/edition-guide/rust-2021/IntoIterator-for-arrays.html
[multiple times]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/IntoIterator-box-slice.html

This also includes the case where `TryFrom` and `TryInto` were added to the prelude for [future editions] due to method resolution issues.

[future editions]: https://doc.rust-lang.org/nightly/edition-guide/rust-2021/prelude.html

## Macro resolution

Due to a current bug in the compiler(?), unstable macros have the same priority as stable macros and macro additions can have noticeable effects on crates *even when unstable*. This was encountered when attempting to create the [`assert_matches!` macro].

[`assert_matches!` macro]: https://github.com/rust-lang/rust/issues/82913

## Unspecified behavior

In general, users of the standard library shouldn't rely on undocumented guarantees of APIs, but sometimes, it happens anyway. Every beta release of Rust is run through [crater] to find regressions across the ecosystem, and sometimes, this means that the library team will need to FCP changes that already were made, or ones that can technically be made within our guarantees.

[crater]: https://rustc-dev-guide.rust-lang.org/tests/crater.html

In general, any documentation change which represents a new guarantee for an API should have FCP approval, and any implementation change which might substantially affect users should *also* have FCP approval, at the discretion of the [FCP team].

[FCP team]: ../membership.md#fcp-membership

If a change does get made but crater reports too many breakages, the team may opt to work with crate maintainers to fix the issue before stabilizing a feature, or implement [dedicated compiler workarounds](./changing.md#edition-dependent-resolution).

## `#[fundamental]`

Normally, the orphan rule allows adding new trait implementations to types defined in a crate without worrying about ecosystem breakage. However, for some types, this actually becomes impossible, as is the case with:

* `&T`
* `&mut T`
* `Box<T>`
* `Pin<T>`

In all of these cases, even though the parent type (`&_`, `&mut _`, `Box<_>`, and `Pin<_>`) is defined in the standard library, downstream crates can
add trait implementations as long as `T` is a type added in their own crates.

This means that all of a sudden, stabilizations for traits are a very big deal, since we need to decide before stabilization whether *any* fundamental types should be included, or risk never being able to include them.

## `#[non_exhaustive]`

`#[non_exhaustive]` is a useful tool that should be applied to enums, structs, and enum variants *before* stabilization, or else it might be impossible to add.

`#[non_exhaustive]` enums specifically allow for extra variants to be added in the future without any breaking changes. This is especially important for [`std::io::ErrorKind`], which has new variants added all the time.

[`std::io::ErrorKind`]: https://doc.rust-lang.org/nightly/std/io/enum.ErrorKind.html

`#[non_exhaustive]` structs specifically allow for extra fields to be added in the future, if the struct otherwise has only public fields. For enum variants, this is especially important, since their fields are always public.

## Compiler intrinsics

Compiler intrinsics, located in [`std::intrinsics`], provide special features that Rust would otherwise not be able to do normally. For example, atomic operations in [`std::sync::atomic`] are implemented using compiler intrinsics, since these otherwise have no way of being represented in Rust. Despite the name being *compiler* intrinsics, in general, intrinsics that are exposed by standard library APIs are guarantees of the *language* and have to be approved by the language team when stably exposed for the first time.

[`std::intrinsics`]: https://doc.rust-lang.org/nightly/std/intrinsics/index.html
[`std::sync::atomic`]: https://doc.rust-lang.org/nightly/std/sync/atomic/index.html

Although the exact naming of compiler intrinsics is left unstable, any implementation of a compiler for Rust has to have them to implement the standard library, and thus they represent a language-level guarantee. Additionally, the specific behavior of intrinsics may be of interest to the operational semantics (opsem) subteam of lang as well.

That said, some compiler intrinsics are just implementation details and *do not* have to be implemented by every compiler to work correctly. These intrinsics are tagged with the `#[miri::intrinsic_fallback_is_spec]` and have a relevant pure-Rust implementation that can be used by `miri` without changing any language-level guarantees. The language team did a blanket FCP allowing the stabilization of these intrinsics without their approval in [#161081].

[#161081]: https://github.com/rust-lang/rust/pull/161081#issuecomment-5344298013

A good rule of thumb for telling the difference between *internal* intrinsics and *language-level* intrinsics is that internal intrinsics will generally depend on target-specific behavior (for example, floating-point arithmetic) whereas internal intrinsics will not (for example, integer arithmetic).

In addition to the initial FCP of intrinsics from the lang team, an additional FCP is required for stabilizing the use of intrinsics in `const` context. The first FCP involves changing the `#[unstable]` attribute to `#[stable]`, and the second FCP involves changing the `#[rustc_const_unstable]` attribute to `#[rustc_const_stable]`. Sometimes, these two FCPs may be combined if `const`-stabilizing an intrinsic is uncontroversial.
57 changes: 57 additions & 0 deletions src/libs/apis/fixing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Fixing APIs

So, we actually lied: some breaking changes can actually be made in the standard library, assuming we do so very carefully. This page lists the shenanigans the library team has performed to get around stability teams, to hopefully be expanded infrequently.

## Deprecation

Sometimes, an API is just bad, and we want to forget it happened. For example, [`std::fs::soft_link`] was deprecated in Rust 1.1.0 and replaced by OS-specific functions, since Windows needs to distinguish between directory and file links.

[`std::fs::soft_link`]: https://doc.rust-lang.org/std/fs/fn.soft_link.html

Other times, we make a feature better and want to forget when it wasn't. For example, the [`try!`] macro was deprecated in Rust 1.39.0 and replaced by the dedicated `?` operator, which allows types like [`Option`] in addition to [`Result`].

[`try!`]: https://doc.rust-lang.org/std/macro.try.html
[`Option`]: https://doc.rust-lang.org/std/option/enum.Option.html
[`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html

## Deprecated safety

Sometimes, making a function safe was a mistake, and we want to mark it as unsafe after the fact. For example, [`std::env::set_var`] is genuinely unsafe, potentially beyond the point of usability on some platforms. And yet, before Rust 2024, this method was completely safe to call, and its unsafety was only added on an edition boundary.

[`std::env::set_var`]: https://doc.rust-lang.org/std/env/fn.set_var.html

## Edition-dependent prelude

Some traits are so useful to Rust that we add them to the prelude, including them in all Rust code by default. However, [this can sometimes break code](./stabilization.md#method-resolution), and thus be a breaking change.

To get around this, the standard library prelude depends on the current edition of Rust, and traits can be added or removed at an edition boundary:

* [Rust 2021] added [`TryFrom`], [`TryInto`], and [`FromIterator`] to the prelude.
* [Rust 2024] added [`Future`] and [`IntoFuture`] to the prelude.

[Rust 2021]: https://doc.rust-lang.org/nightly/edition-guide/rust-2021/prelude.html
[Rust 2024]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/prelude.html
[`TryFrom`]: https://doc.rust-lang.org/nightly/std/convert/trait.TryFrom.html
[`TryInto`]: https://doc.rust-lang.org/nightly/std/convert/trait.TryInto.html
[`FromIterator`]: https://doc.rust-lang.org/nightly/std/iter/trait.FromIterator.html
[`Future`]: https://doc.rust-lang.org/nightly/std/future/trait.Future.html
[`IntoFuture`]: https://doc.rust-lang.org/nightly/std/future/trait.IntoFuture.html

## Edition-dependent resolution

Sometimes, adding a new trait implementation or method breaks existing code in ways we really can't ignore, and we want to add them anyway. In these cases, we explicitly modify the compiler to avoid a method before an edition boundary:

* [Before Rust 2021], [`IntoIterator`] for arrays is hidden.
* [Before Rust 2024], [`IntoIterator`] for boxed slices is hidden.

[`IntoIterator`]: https://doc.rust-lang.org/nightly/std/iter/trait.IntoIterator.html
[Before Rust 2021]: https://doc.rust-lang.org/nightly/edition-guide/rust-2021/IntoIterator-for-arrays.html
[Before Rust 2024]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/intoiterator-box-slice.html

## Edition redirects

In the future, the standard library will be able to "redirect" paths to different places based upon the edition. This unlocks many new possibilities.

However, it is not yet implemented: [#160227]

[#160227]: https://github.com/rust-lang/rust/pull/160227
20 changes: 20 additions & 0 deletions src/libs/apis/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Maintaining APIs

Compared to third-party crates on crates.io, the Rust standard library has much stronger stability guarantees. Any stable API that's added to the standard library can *never* be removed or modified in a backwards-incompatible way, even if we do have [a few ways to get around this](./changing.md).

In some cases, APIs will need special language or compiler support, and these have to exist in the standard library. For example, the `include!` macro is a standard library API, but the API exposes a feature specified in the language and implemented in the compiler. The library team works closely with the language and compiler teams on these kinds of features.

This also indicates a few more cases where standard library support makes sense, since the compiler has very robust testing infrastructure: if an API is performance-sensitive, we can add additional compiler intrinsics to support it, or codegen tests to verify its correctness. We also run tests for several different systems and generally have the capacity to maintain cross-platform functionality better than most crates.

APIs can also be substantially more ergonomic in the standard library. If you want to use a new method on a primitive type like `u32` without importing a separate trait, it has to live in the standard library.

As nice as standard library APIs can be, as mentioned earlier, they represent a very strong commitment that has to be taken seriously. And here are some of the ways we do that:

- [API Change Proposals (ACPs)](./proposals.md)
- *Most API changes start with an API Change Proposal.*
- [Changing APIs](./changing.md)
- *Not all API changes are obvious, or even API-related.*
- [API stabilization](./stabilization.md)
- *Every new guarantee requires team consensus.*
- [Fixing APIs](./fixing.md)
- *We can't break APIs, but can we fix them?*
Loading
Loading