Welcome

Welcome to the repository for the Rust Language Design Team. This page stores our administrative information, meeting minutes, as well as some amount of design constraints. It's always a work-in-progress (insert omnipresent mid 90s logo for under construction here).

What is the lang team

The lang team generally governs the "surface area" of the language, meaning both what code compiles and what happens when it executes. For new language features, we also assume a general "project management" role, in that we track the feature as it progresses from an idea, to implementation, and finally to stabilization.

Note that implementation itself is governed by the compiler team. To be very concrete, the lang team controls the spec, and the compiler team approves the implemenation and ensures that it meets the spec. Naturally, though, development is a collaborative process, and it often happens that the spec is altered in response to concerns that arise during implementation.

We also work closely with the types team, which owns the details of the type system design in much the same way as the compiler team owns the details of the implementation.

Finally, there are often "grey areas" between the language and library teams, such as the addition of a new standard library trait that reflects a core system capability (e.g., Future). In those cases, the lang team is generally reponsible for deciding if we want the core capability, the libs team owns the API details.

Lang team "how to" guide

This section includes instructions on how to do various interactions with the lang team.

Nominate an issue

You can raise issues to lang team attention by tagging them with I-lang-nominated. We scan through nominated issues during our triage meetings. For each issue, we try to answer questions and reach decisions on the question being discussed in the issue.

How to nominate

Add a self-sufficient comment to the issue that explains why you are nominated the issue and includes the text @rustbot label +I-lang-nominated. For example:

@rustbot label +I-lang-nominated

I am nominating this for lang-team attention. We have been discussing the pros/cons of updating the type-checking rules for foo bar. The options on the table are as follows:

* Allow type mismatches: This is good because blah.
* Disallow type mismatches: This is good because blah.

Where should we go from here?

The ideal comment will identify precisely what question you would like answered. Please try to make the comment easy for us to parse and understand without requiring a lot of context. We encourage links to internals or Zulip so we can dive into the details, but it really helps us give useful answers if you can summarize the key details up front.

If your comment requires more than 5-10 minutes of reading and discussion to understand and effectively respond to, consider filing a meeting proposal instead. This will give you ~60 minutes to present your question and the lang team more time to analyze it. We may punt the question back to you with an ask to do so if the question isn't answerable in our triage meeting time.

How quickly will the lang team answer?

We try to be prompt, but sometimes we are not. Othertimes, we discuss the issue, but fail to leave the follow-up comment, because we're only human. Please feel free to raise the topic on Zulip or reach out to a lang team member.

Repositories where we look for nominations

Nomination is currently supported on the following repositories:

  • rust-lang/rfcs
  • rust-lang/rust
  • rust-lang/reference
  • rust-lang/lang-team

(This set is defined by the nominated list in the triagebot source code)

Proposing a topic for a design meeting

You can propose a topic for a design meeting by opening an Design meeting proposal issue on the lang-team repository. We schedule meetings in our monthly planning meeting.

Every design meeting begins by reading a document and leaving comments. That document must be prepared 24 hours in advance of the design meeting and posted on the issue. If you propose an issue, you should be willing to prepare that document, or else indicate who will do the preparation.

Typically, design meetings are associated with active initiatives: you may wish to propose an initiative instead.

Proposing a change to the language

Do you have an idea for a new language feature? The following page describes the lang team policy around changes to the language.

TL;DR

The highlights are these:

  • An RFC is required for most changes.
  • Small changes that fit in a PR, have narrow impact, and are uncontroversial can skip the RFC process (read more).
  • If you have a lang-team liaison and an experienced implementor, you can start experimenting by just adding a feature gate and a tracking issue, but you'll still need an RFC later.

Complete flowchart

The following lays out the complete flow chart for language features. Note that you can click on most of the nodes to read about that step in more detail.

flowchart TD

subgraph LangTeamChangeProcess [Lang Team Change Process]
TweakToExistingFeature["How large is your proposed change?"]
LangTeamLiaison["Are you an experienced contributor\nwith a lang team liaison?"]
NeedToExperiment["Do you need to experiment\nbefore you can write RFC?"]

subgraph Stages
    ExperimentalFeatureGateProposed["Create a tracking issue and\nopen a rust-lang/rust PR proposing\nan experimental feature gate"]
    ExperimentalFeatureGateAccepted["Experiment is approved. Write code!"]
    RFCOpen["Open a rust-lang/rfcs PR\nwith the completed RFC"]
    RFCAccepted["RFC is merged.\nIf needed, create a tracking issue,\n and finalize the implementation"]
    FeatureComplete["Implementation is complete.\nGain experience with the new feature."]
    StabilizationProposed["Author a stabilization report and\nopen a PR stabilizing the feature"]
    Documented["Author a rust-lang/reference PR\nextending the reference\nto describe the new feature."]
    StabilizationAccepted["Stabilization is approved!\nFeature rides the trains into stable."]
    ChangeProposed["Open a rust-lang/rust PR and\nnominate for lang team."]
    ChangeAccepted["PR is merged."]
    TypesTeamApproval["Types team approves the\ninteraction with type system."]
    StyleTeamNotified["Style team notified about the new feature."]
    
    ExperimentalFeatureGateProposed --Seconded--> ExperimentalFeatureGateAccepted
    ExperimentalFeatureGateAccepted --> RFCOpen
    RFCOpen --FCP--> RFCAccepted
    RFCAccepted --> FeatureComplete
    FeatureComplete --> StabilizationProposed
    %% StabilizationProposed --> StabilizationAccepted
    StabilizationProposed --do this--> Documented
    Documented --> StabilizationAccepted
    StabilizationProposed --and this--> TypesTeamApproval
    TypesTeamApproval --> StabilizationAccepted
    StabilizationProposed --and this--> StyleTeamNotified
    StyleTeamNotified --> StabilizationAccepted
    ChangeProposed --FCP proposed and accepted--> ChangeAccepted
    ChangeProposed -- If team feels change\nmerits a second RFC --> RFCOpen
end

TweakToExistingFeature -- Tweak to an existing aspect of Rust --> ChangeProposed
TweakToExistingFeature -- New feature or a complex change --> LangTeamLiaison
LangTeamLiaison -- Yes --> NeedToExperiment
LangTeamLiaison -- No --> RFCOpen
NeedToExperiment -- Yes --> ExperimentalFeatureGateProposed
NeedToExperiment -- No --> RFCOpen
end

%% Drawn from https://coolors.co/25283d-8f3985-a675a1-cea2ac-efd9ce
classDef pink fill:#EFD9CE
classDef tuscany fill:#CEA2AC
class LangTeamChangeProcess pink
class Stages tuscany

click RFCOpen href "https://github.com/rust-lang/rfcs/#when-you-need-to-follow-this-process" "Read about RFCs"
click RFCAccepted href "https://forge.rust-lang.org/lang/rfc-merge-procedure.html" "RFC merge procedure"
click ExperimentalFeatureGateProposed href "./experiment.html" "Read about experimental feature gates"
click ExperimentalFeatureGateAccepted href "./experiment.html" "Read about experimental feature gates"
click StabilizationProposed href "./stabilize.html" "Read about stabilization procedure"
click StabilizationAccepted href "./stabilize.html" "Read about stabilization procedure"
click Documented href "./stabilize.html" "Read about stabilization procedure"
click TypesTeamApproval href "./stabilize.html" "Read about stabilization procedure"
click StyleTeamNotified href "./stabilize.html" "Read about stabilization procedure"
click TweakToExistingFeature href "./propose.html#what-constitutes-a-small-addition-or-tweak-to-an-existing-feature"
click LangTeamLiaison "./experiment.html" "Read about experimental feature gates"
click NeedToExperiment "./experiment.html" "Read about experimental feature gates"

Frequently asked questions

What do the labels "lang-team second" and "lang-team consensus" mean?

These refer to our decision process:

  • A second means that some lang team member must liaison the idea, but it doesn't require full checkboxes. Instead, the idea goes immediately into "final comment period" (which lasts for 10 days), giving other lang team members a chance to comment on it and raise concerns. We use seconding for reversible decisions that don't commit the language to anything in particular.
  • A consensus means that every lang team member must check their box and actively approve.

In both cases, we currently handle consensus with rfcbot (@rfcbot fcp). For a second, however, the lang team member who initiatives FCP can go ahead and check the boxes of other lang-team members, they can raise concerns if needed. This is a temporary measure until we have first-class support for seconding.

Are RFCs required for every language change?

No! For small additions or tweaks to existing features, you can simply implement the change, open a PR, and nominate it for lang-team attention. If the change turns out to be complex or controversial, though, we may close the PR and request an RFC instead.

What constitutes a "small addition or tweak to an existing feature"?

We do not require an RFC for everything. Small changes that fit in a single PR, have narrow impact, and are uncontroversial can skip the RFC process. Simply make the change, open the PR, and nominate it for lang-team feedback. But be aware that we may still ask you to write an RFC! The rule of thumb is that we use RFCs for ideas that impact a lot of users or are potentially controversial. They're a great way to get broad feedback from the community about an idea.

If you choose to open a PR without an RFC, please document the motivation and details of your change! Very often people will open a PR that changes some code in the compiler without clearly explaining what they are trying to achieve. Also, please make the explanation "self sufficient" -- avoid linking to internals threads or other places where we have to read a bunch of context to understand what is going on (it's encouraged, however, to provide a summary and link to threads for more details).

Some examples where RFCs are typically NOT required...

  • Narrow changes like adding support for a new ABI (this may even just be a compiler concern).
  • Soundness fixes to existing features, unless they have large impact or change the way people have to write Rust, in which case we may opt for an RFC.
  • Small changes to complex rules, such as name resolution, intended to address a particular problem and which don't impact most users; often in cases like these RFCs do not provide valuable feedback, and so we'll instead focus on getting the relevant experts to weigh in. This category in particular still needs to have a detailed write-up.
  • Extending a lint to cover more instances of the same general pattern, or to be more precise.
  • Proposing a new lint that is narrow in scope.

What is the policy for adding a new lint?

We have a streamlined process for most lint proposals. See the How do I add a new lint? for details.

Do experiments always require an RFC?

In the diagram above, we show experiments as always leading to an RFC. This is typically the case because experiments tend to be for larger, more ambitious features. However, if the experiment turns out to be a relatively small change to the language -- i.e., some change that would not require an RFC anyway -- then you can skip the RFC and move straight to stabilization.

What about adding special traits and things to the standard library?

We consider intrinsics and "lang item" (things that need special treatment from the language) to be under the lang team purview. Specifically, the lang team governs the semantics and capabilities exposed by this new feature. The details of the API are governed by the libs-api team.

Whenever possible, though, we prefer to issue a quick approval for the "general feature" being discussed, and leave it to the libs-api team to decide where to apply it. For example, we approved the ability to add inherent methods to standard library types like u32 long ago, but the libs-api team governs what APIs are available.

My RFC has been waiting for a comment from the lang-team for a long time!

First off, I'm sorry, that sucks. We are aware that we need to do a better job keeping up with RFCs. :/ That said, what you can do is to nominate the RFC and we will discuss it during the meeting.

How do I propose a new lint, or extend an existing one?

For small lints, you can follow the policy for smaller changes: implement the lint, open a PR, and then nominate it for lang-team attention.

We may request an RFC if:

  • the lint is on (warn or higher level) by default, and is expected to affect a lot of users
  • the lint is controversial
  • the lint sets a (new) direction for Rust -- for example, changing an existing pattern to a different one, even if the pattern isn't widely used
    • e.g., deprecating a syntax to make room for a possible new language feature

If in doubt, you can always raise the idea on Zulip first.

Experimental feature gates

We use "experimental feature gates" to allow experienced Rust contributors to start implementing and exploring features even before an RFC has been written. This is particularly useful for larger features where we know we definitely want to solve the problem, but there are a lot of unknowns to work out before we can really create a coherent RFC -- think of things like adding async functions or the like.

Process

If you are an experienced Rust contributor who would like to start an experiment in-tree, the process is as follows:

  • Write-up a description of the problem you are trying to solve and the general shape of the solution you want to work on. Discuss it on Zulip or elsewhere to find a lang-team liaison:
    • The liaison is the connection to the lang-team. They can check in with you from time to time to see how the work is going and relay those updates to the lang-team (of course, you're always welcome to join meetings yourself too!). They can also help to discuss problems that arise.
  • Once you've found a liaison, open a PR adding a new feature gate to the compiler and create an associated tracking issue.
    • The PR and tracking issue should include a write-up documenting the motivation and outline of what they are trying to achieve.
    • The feature gate should be marked as 'experimental', so that users get warnings if they try to use it. This flag has to stay until an RFC is accepted, even if the implementation is in good shape.
  • The lang-team liaison will "second" the PR, starting an FCP. Once the FCP completes, the PR can land and implementation work begins (always gated under the new feature gate).
    • Approving a new feature gate does not imply support for the feature. It implies only that the lang team thinks it is worth doing the experiment to see what results.
    • Note to lang team members: If you have concerns about the feasibility or wisdom of the feature, the right course of action is usually to allow experimentation to continue, but ensure that your concerns are noted on the tracking issue. This allows the experimentors to try and gather data and address your concern.
  • When you feel the design is ready, you write an RFC as normal with your proposal. The goal of the experimentation period is simply to gain experience and information so that a better RFC can be authored.

Frequently asked questions

What is the role of the lang team liaison?

The lang team liaison is the connection to the lang team. They should be available to discuss progress and generally track what's going on, and they can also raise questions to the broader lang team during triage meetings and the like (of course, the meetings are open, so you're also welcome to join if you are able).

I've got an idea, how do I find a lang-team liaison?

We don't really have a process for that, but circulating the idea on Zulip is a good idea, or perhaps reach out to lang-team members that you know.

Why is experimentation limited to experienced contributors?

We've found that it works best when the person driving the experiment is able to move independently and without mentoring. Most folks on the lang team have limited bandwidth, so when they agree to serve as liaison, they are committing to meet regularly, give feedback on your progress, and to circulate ideas within the lang team, but they are not necesarily going to have time to help find solutions to problems beyond that ((many lang team members aren't that familiar with the compiler details anyway).

What if I've got an implementation on a branch already?

What we're really looking for in experimentors is commitment and the ability to see the work through. If you're able to implement the idea in a branch, that's good evidence. See if you can find a lang-team liaison.

Can a lang-team member propose an experiment, too?

Lang-team members can be the one to propose an experiment, and can serve as their own liaison, but they should find someone else to "second" the FCP on the feature gate PR.

What if I'm not an experienced contributor, but I have a mentor who is?

That's fine, you can still open the PR, but your mentor should be the one to nominate it for lang-team consideration.

What if I'm an experienced contributor, and I want to mentor someone?

See the previous question.

As the experimentor, what do I do when I feel like I am ready to write the RFC?

Glad to hear the experiment was a success! Check in with the liaison to figure out if they feel like it's time to author the RFC. In particular, if people raised concerns about the design in the beginning, make sure that you have a good answer for them. Even better, show them the answer, and see if they are convinced!

As the experimentor, what if I feel like we don't want the feature after all?

This is also a very useful finding! In this case, it's best to write up a comment (potentially short) on the tracking issue reflecting the findings from the experimentation phase, and suggest to your lang team liaison that the tracking issue may want to be fcp'd to close so we can remove the feature from the compiler.

It's generally a good idea to do this for features that aren't being actively driven to conclusion and are in the experimental phase, as they can easily accumulate otherwise.

As the experimentor, what if I run out of time to drive this proposal?

First off, it's always ok to take a break. If you are going to step away for a long time, you should at minimum leave a comment with your current thinking on things -- if nothing else, it'll help you remember what was going on when you come back.

If you feel like you have to step away indefinitely, then discuss with the liaison. They may be able to find someone else, or it may make sense to simply write-up your findings and remove the feature from the compiler.

As a lang team member, what do I do if I feel like my concerns are not being addressed?

Bring up your concerns with the lang team liaison and try to work towards finding experiments or new approaches that can resolve them. If this feature is ever going to make it to RFC, they are going to need your agreement, after all!

What if I have a competing proposal? Can I land my own experimental feature?

Obviously the best is if you can combine your idea with the existing experiment, particularly if your idea is a minor variation. But it is also ok to have multiple competing experiments, if the idea is going in a different direction. Just follow the same process (find a lang team liaison, etc). Be sure to note in the PR etc that you know this overlaps with the existing experiment and are looking to explore a different part of the design space. Also, it will be really helpful if you and the other experimenters can jointly maintain a kind of FAQ comparing and contrasting the two ideas.

Stabilize a feature

The final step in the language-change process is to stabilize a feature. Stabilization works as follows:

  • Author a stabilization report:
    • Briefly recap the feature's design from the RFC -- you don't have to go into detail, we can re-read the RFC.
    • Give detailed descriptions of how the feature's design has changed since the RFC was approved!
    • Summarize any major decisions that were made during the implementation process.
    • Verify that the feature is fully implemented. Look for tests covering all the major pieces of the RFC and include them in the stabilization report.
    • Provide answers to any "unresolved questions" listed in the RFC.
    • Describe the implementation history of the feature (optional).
  • Prior to stabilizing, we need to coordinate with other teams:
    • An open PR editing the reference to describe the change is required. (You don't personally have to author it, but there needs to be an open PR, and ideally one that has been edited and is approved modulo actual stabilization by one of the repo maintainers.)
    • If the feature affects the type system, you should nominate for @rust-lang/types to check for their approval.
    • If the feature adds new syntax, you should should nominate for the style team to get their approval.
  • The lang team will read the report and eventually move to FCP. Per our decision process, full consensus is required, as this is an irreversible change.

See also the rustc-dev-guide's stabilization guide.

This section documents the work-in-progress Rust language team decision process. This process, and the rustbot tooling to support it, does not yet have a finished implementation. This document serves to explain the intended process, for the purposes of ongoing implementation.

Prioritized principles of Rust team consensus decision-making

These are in order of priority. They're intended to be general enough that they could apply to any Rust governance team, not just the language team.

  • Treasure dissent. When someone raises a concern, that's a chance to improve the design, and to discover and explore underlying values. Dissent should be an amicable, cooperative process.
  • Understand and cooperatively resolve concerns. We cannot resolve a concern without first understanding it, including the underlying values motivating it. We should demonstrate that understanding by documenting the concern. We should consider the tradeoffs and the impacts on users, through the Rust design principles. We should seek out and favor satisfying solutions (those that satisfy everyone's values) over satisficing solutions (those that are just good enough for people to accept them as a compromise among conflicting values, without actually being happy with the outcome).
  • Don't force an irreversible decision. We should make decisions reversible whenever we can. When making a necessarily irreversible decision (e.g. stabilizing a feature), we should pay close attention to dissent, and hesitate before overriding objections. If possible, we should seek a better alternative, or seek common ground we can find consensus on, or seek an intermediate step that addresses the same use case and supports evaluation for a more informed decision in the future. If none of those are possible, consider the null alternative; not making a change should always be the easier path, and the burden of proof to override a concern on an irreversible decision should be high.
  • Value expertise. When cooperatively resolving a concern, or when considering overriding a concern, carefully weigh the advice and recommendations of experts. This includes team advisors, domain experts, and the owners or members of relevant initiatives.
  • Recording reasoning helps ensure good, consistent decisions over time. Even if we decide not to sustain an objection, we should always record the objection and the reasons for our decision as a "dissent", as well as any unresolved questions for evaluation later in the process. The team member who raised the objection has the perogative to author that dissent and frame the unresolved questions (within reason).
  • Consensus doesn't mean unanimity. Consensus means everyone is heard and understood, and all concerns are addressed (even those not treated as blocking), and the team finds the outcome reasonable. Consensus does not mean everyone agrees completely.

Consensus decision-making process

First, see some examples of the decision-making process in action. Then, read the decision process reference for the full process and the rustbot tooling to support it.

Examples of the decision-making process in action

Reversible decision: merging a proposal

The process is best described by example. Suppose that there is a pending lang team proposal, and a lang team member would like to serve as the liaison. They contact the team leads and receive the go-ahead. They can then write:

@rustbot merge

I propose to merge this proposal. I think it will be a great addition to Rust!

This indicates that they would like to merge the proposal. At the moment, there is no decision pending, so rustbot would add a comment that looks like the following:

Hello! @Alan has proposed to merge this. This is a reversible decision, which means that it will be affirmed once the "final comment period" of 10 days have passed, unless a team member places a "hold" on the decision (or cancels it).

Team memberState
@Alanmerge
@Barbara
@Grace
@Niklaus

As the comment says, the PR is now in "pending decision" state, with Alan having kicked off the process with a proposal to merge. Alan's status of merge will link to his comment.

Now, for this particular proposal, Barbara has a concern. She thinks that the proposal has overlooked an important consideration. She writes a comment:

@rustbot hold

Did you consider reversing the polarity? Or the impact on the flux capacitor?

At this point, rustbot updates the state; Barbara's status of hold will link to her comment:

Hello! @Alan has proposed to merge this. This is a reversible decision, which means that it will be affirmed once the "final comment period" of 10 days have passed, unless a team member places a "hold" on the decision (or cancels it).

Team memberState
@Alanmerge
@Barbarahold
@Grace
@Niklaus

Alan is currently busy at work, though, so by the time that he and Barbara get a chance to talk, 11 days have passed. (Alan and Barbara receive a ping from rustbot after a week or so.) Once they get a chance to talk, Alan fully addresses Barbara's concern, so Barbara posts:

@rustbot merge

The comment is updated:

Hello! @Alan has proposed to merge this. This is a reversible decision, which means that it will be affirmed once the "final comment period" of 10 days have passed, unless a team member places a "hold" on the decision (or cancels it).

Team memberState
@Alanmerge
@Barbarahold merge
@Grace
@Niklaus

Barbara's previous hold status links to her previous comment setting her status to hold, and her current merge status links to her more recent comment setting her status to merge.

At this point, all the statuses are either empty or merge, and more than 10 days have passed since the FCP started. Therefore, it completes immediately.

Authoring an RFC (illustration of rustbot restart)

After some time, the proposal is completed and an RFC is proposed. This is a reversible decision. Alan, as the liaison, proposes to merge the RFC with @rustbot merge, and the decision making process proceeds as above.

This time, Niklaus has a concern:

@rustbot hold

I have not had time to read this yet! Give me a bit of time to write it up.

After 7 days have passed, rustbot writes to him:

@Niklaus, I see you have placed a hold but 7 days have passed. Are you any closer to reaching a decision? (cc @Alan)

This continues for a week or two while Alan and Niklaus play "email tag". In the interim, Barbara decides she agrees with the RFC, so she uses @rustbot merge as well. The status now looks like this:

Hello! @Alan has proposed to merge this PR. This is a reversible decision, which means that it will be affirmed once the "final comment period" of 10 days have passed, unless a team member places a "hold" on the decision (or cancels it). decision.

Team memberState
@Alanmerge
@Barbaramerge
@Grace
@Niklaushold

Eventually, Alan and Niklaus find a time to discuss, and Alan agrees that Niklaus's concerns are valid, so he makes some major edits to the RFC. Given that the RFC is completely different, he decides to restart the clock and writes:

@rustbot restart

This strikes through the state of all team members (setting their current status to blank, while preserving the history) and begins the clock anew. rustbot also pings the relevant team members:

Dear @rust-lang/team, @Alan has restarted the clock!

The status now looks like this:

Hello! @Alan has proposed to merge this. This is a reversible decision, which means that it will be affirmed once the "final comment period" of 10 days have passed, unless a team member places a "hold" on the decision (or cancels it).

Team memberState
@Alanmerge
@Barbaramerge
@Grace
@Niklaushold

Barbara can use @rustbot merge to re-affirm her merge status, and Niklaus can use @rustbot merge to set his own status to merge since he agrees with the resolution of his concern.

Authoring an RFC continued (Overriding a concern)

At this point, Grace has a concern, and explains that concern in detail:

@rustbot hold

I've thought about this a lot, and I don't think we should do this. Now that I see the syntax used in practice, I feel like if we do this it'll have an adverse effect on the ecosystem...

The status now looks like this:

Hello! @Alan has proposed to merge this. This is a reversible decision, which means that it will be affirmed once the "final comment period" of 10 days have passed, unless a team member places a "hold" on the decision (or cancels it).

Team memberState
@Alanmerge
@Barbaramerge merge
@Gracehold
@Niklaushold merge

Niklaus reads this message. He feels he understands the concern well, and agrees that this point hasn't yet been considered:

@rustbot hold

I agree. I think we should take more time to evaluate alternative syntaxes. What about...

Over the course of a few subsequent meetings and side conversations, Grace and other team members discuss the concern further; the initiative owner also considers the concern, and raises it with others working on the initiative.

The owner of the initiative updates the RFC to include a discussion of a couple of alternative syntax proposals. The owner recommends a slightly modified version of the originally proposed syntax, and outlines criteria that they feel the syntax should meet in order to support the use case.

Grace agrees that her concern has been understood, but does not agree with the proposed syntax. Grace feels the new proposal is an improvement, but her concern remains.

Niklaus feels that the team has understood Grace's concern, and furthermore, that the updated proposal addresses Grace's concern:

@rustbot merge

I appreciate the potential impact this may have on the ecosystem. However, I feel that as now described in section XYZ of the RFC, the value of A outweighs the risk of B, and I think C mitigates the potential risk by...

(Notice that while Niklaus feels that the team has understood Grace's concern, he does not speak for the entire team or imply that his summary represents the entire team. Niklaus is just withdrawing his own support for the concern.)

At this point, the entire team other than Grace agrees that the proposal should move forward:

Hello! @Alan has proposed to merge this. This is a reversible decision, which means that it will be affirmed once the "final comment period" of 10 days have passed, unless a team member places a "hold" on the decision (or cancels it).

Team memberState
@Alanmerge
@Barbaramerge merge
@Gracehold
@Niklaushold merge hold merge

(We'll assume, for this example, that Grace does not manage to convince anyone else.)

Grace takes some time, working with the RFC author, to add a dissent, including a specific unresolved question.

Grace then writes a comment containing @rustbot dissent. (If necessary, or if Grace would prefer, another team member may issue @rustbot @grace dissent on her behalf.) The status now looks like this:

Hello! @Alan has proposed to merge this. This is a reversible decision, which means that it will be affirmed once the "final comment period" of 10 days have passed, unless a team member places a "hold" on the decision (or cancels it).

Team memberState
@Alanmerge
@Barbaramerge merge
@Gracehold dissent
@Niklaushold merge hold merge

Since all statuses are now either merge or dissent rustbot also posts a comment:

The final comment period has resolved, with a decision to merge.

Note that this decision has dissents; please ensure these dissents have been recorded for subsequent consideration.

Stabilizing a feature

The feature has been implemented and is now eligible for stabilization. Alan writes a stabilization report and posts it, and then issues the command

@rustbot stabilize

Rustbot recognizes that a "stabilization" decision is irreversible, so the template is a bit different:

Hello! @Alan has proposed to stabilize this. This is a irreversible decision, which means that it will be affirmed once all members come to a consensus and the "final comment period" of 10 days has passed.

Team memberState
@Alanstabilize
@Barbara
@Grace
@Niklaus

This time, Barbara, Grace, and Niklaus must all explicitly provide a status before the decision can proceed. One by one, they join the PR. They must individually set their state using one of the rustbot commands.

Niklaus reads this and comments with @rustbot merge. But then, Niklaus uses the feature and discovers a crucial flaw. He posts a comment:

@rustbot close

After more testing, I believe this is not ready for stabilization. I have found that it doesn't work at all like the specification in the case of foo! This seems closely related to Grace's concern on the RFC; I think if we stabilize at this point we may indeed harm the ecosystem...

Other team members test as well, and find that Niklaus is right. Alan changes his status using @rustbot close, and Grace (with some relief) sets the same status.

Once everyone has changed their status, rustbot posts a comment:

The final comment period has resolved, with a decision to close.

(Note: Since close is an inherently reversible status (a PR can always be reopened), rustbot can observe that everyone has set a reversible status, and will start treating the decision as reversible; this means the final comment period can end even if Barbara hasn't responded yet.)

This may not be the end of this feature's consideration, and the concern might get resolved in many different ways. The initiative owner might need to do some additional design work on how to solve the original use case and address the concern; implementers may find a way to address the concern by improving the implementation; or the team may change their mind.

Decision-making process, detailed description

  • Entering a "decision period" can be done by having a team member tell rustbot an initial status (merge, stabilize, or close; or, reversible or irreversible with a custom identifier).
    • All other team members have no initial status set.
    • During a reversible decision period, if later commenters indicate the decision is irreversible, the decision changes to irreversible.
    • During an irreversible decision period, if all commenters change their status to a reversible status (most commonly close), the decision becomes reversible.
    • Bot commands accept mut/mutable/rev as synonyms for reversible, and immut/immutable/irrev as synonyms for irreversible.
  • Once the "decision period" has begun, a clock of 10 days starts. The clock is never paused unless an explicit @rustbot restart command is given.
  • The restart command sets the decision period back to its initial state (members' current statuses are also set back to blank, though the history of their previous statuses is preserved as with any other status change).
  • A decision is reached when the following conditions are met:
    • At least 10 days have elapsed since the decision period began (or when it was last restarted).
    • Everyone is set to the same status, or to abstain, or to dissent (with at most one dissent status), or (for a reversible decision only) blank.
  • Member may place a hold (raise a concern) at any phase of the process: proposal, experimentation, testing, stabilization. Ideally, concerns should be raised as early as possible.
    • In general, one of the liaison's jobs is to anticipate concerns that may arise and reach out proactively to the members of the team. If we find that team members regularly need to place serious holds for similar reasons, that may indicate we need to work harder at team calibration.
  • "Holding" a decision is simple. If you have a potential concern that you haven't had a chance to fully articulate yet, you get periodic pings.
  • In order to proceed after a concern is raised, whether sustaining the concern or overriding the concern, the team must understand the concern. This understanding should be expressed in writing rather than just verbally. Commonly, the owner of an initiative/proposal may incorporate the concern into the proposal and address it there (whether via their own words or those of a team member).
    • Ideally, a concern is only considered "understood" if the objector agrees it has been. If that is not possible, then everyone on the team other than the objector must unanimously agree that there is no further understanding to be gained. (In practice, this agreement is determined by whether anyone on the team is willing to support the objection.)
  • If the owner feels that the concern has been adequately addressed, they can produce a write-up that describes the concern and request a poll of the team members to see where everyone stands.
    • Sustaining an objection requires one team member other than the objector. A team member should sustain an objection if either they believe the objection has been understood and they agree that it must be addressed, or if they believe the objection has not been fully understood (whether they personally agree with it or not).
    • If everyone else agrees that the concern has been understood and that the current design is sufficient, then the concern is overridden.
    • Team members are encouraged to regularly sustain objections they don't personally agree with, if they believe the objection has not yet been fully understood; this is a normal and valuable part of the process.
  • Whenever a concern is overridden, team members are encouraged to add a dissent into the document to describe their concern and why they don't agree with the decision. They (or another team member) should then set their status to dissent.

Rustbot commands

The following commands are accepted by rustbot. (Commands written below omit the required @ on rustbot to avoid invoking rustbot when quoting the documentation.) A number of comments take one or more optional @member arguments, denoted @member*; if supplied, the command is issued on behalf of those member(s), instead of the person writing the command. (The @ for each member is required.) It is also permitted to write @rust-lang/team to select all members of the team. rustbot will always link from each member's row in the table to each comment changing their status.

  • rustbot @member* reversible ident or rustbot @member* mutable ident (unambiguous prefixes such as mut or rev also work)
    • If not in a decision period: begin a reversible ("mutable") decision, proposing the outcome ident; all other members are set to a blank status.
    • If in a decision period: set yourself to ident. (Decisions do not proceed unless all members have the same status or abstain.)
  • rustbot @member* irreversible ident or rustbot @member* immutable ident (unambiguous prefixes such as immut or irrev also work)
    • If not in a decision period: begin an irreversible ("immutable") decision, proposing the outcome ident; all other members are set to a blank status.
    • If in a decision period: set yourself to ident. If the decision was previously considered reversible, change it to irreversible. The decision now requires full team consensus. (Members may set a status of abstain on the decision if they wish.)
  • rustbot @member* merge
    • Alias for rustbot @member* reversible merge - a reversible decision with the proposed outcome merge.
  • rustbot @member* close
    • Alias for rustbot @member* reversible close - a reversible decision with the proposed outcome close.
  • rustbot @member* stabilize
    • Alias for rustbot @member* irreversible stabilize - an irreversible decision with the proposed outcome stabilize.
  • rustbot @member* abstain
    • If not in a decision period: error
    • If in a decision period: set your status to abstain. This status does not block a decision.
  • rustbot @member* hold
    • If not in a decision period: error
    • If in a decision period: set your status to hold
      • Every N days while a hold persists, rustbot will ping all members who have status hold (and potentially other involved team members as well)
  • rustbot @member* dissent
    • Equivalent to abstain, except that it sets a status of dissent. This status on one team member does not block a decision. A status of dissent on two or more team members will block a decision.
    • Note that dissent should not be set when first raising a concern, only after attempts to resolve the concern have been unsuccessful.
  • rustbot restart
    • If not in a decision period: error
    • If in a decision period: set all members other than the one issuing the command to have a blank status. Preserve the last set status of the person issuing the restart.
    • Note: if the last set status of any team members were irreversible, the decision will continue to be treated as irreversible until all such members set an explicit status otherwise.
  • rustbot cancel
    • If not in a decision period: error
    • If in a decision period: cancel the decision period.
    • Note that if a subsequent decision is started in the same issue, rustbot should link to the previous decision summary table.

Frequently asked questions

Why can members override other members positions?

It's quite common to want to check boxes and similar on behalf of all the people in a meeting. It's also annoying to have to restart a decision process (e.g. closing and reopening with rfcbot) just to be able to close concerns on someone else's behalf (e.g. people who have left the team, or people who raised a concern on behalf of someone else not on the team, or similar). We can trust each other on the team. If people abuse rustbot to disrupt the process, that isn't a problem to be solved with tooling.

Do we want to require "all but N" people to affirm a decision, as rfcbot does?

We opted to require 100% participation on irreversible decisions such as stabilization, but not on very lightweight reversible decisions such as starting an initiative. We believe that lang team members can be expected to at least leave a comment (even if just rustbot abstain), but in the limit we also have the option to set a status on another member's behalf.

Why does the timer start when the decision period starts, and not when a consensus is reached?

The current rfcbot starts the 10 day FCP timer once a consensus is reached. However, we have observed that, in practice, the "holdouts" on consensus are typically the lang team members. Further, this seems to assume a "two-phased" decision making model where folks outside the team are commenting only after the lang team has reached a consensus. In practice, both decision-making and commenting tend to be more fluid, and hence we would prefer not to have a long delay after we reach consensus.

Why not use checkboxes?

This process recognizes that sometimes, in the flow of conversation, the decision to merge can transmute into a decision to close, or there may be multiple potential outcomes.

The presentation of statuses also makes it easy for rustbot to present the history of status changes, with links to team members' messages.

Why is close always reversible?

Because, well, it is! We can always re-open a PR or issue, after all.

What about "postpone"?

Just write it in the comment for a @rustbot close. In practice, a "close" does not preclude reopening later, and a "postpone" does not guarantee reopening later.

Is "Don't force an irreversible decision?" absolute?

No; it's a strongly held principle, but not an absolute one. Sometimes we may have to make an irreversible decision, even in the face of dissent. However, we should be extraordinarily careful when doing so, and in particular, we should consider very carefully whether we could make a reversible decision, or find a better consensus, or whether the consequences of not making the decision outweigh the consequences of making it. This should be an extremely rare event.

The previous decision-making process based on rfcbot allowed indefinitely blocking concerns. This new process introduces a means of carefully resolving such concerns, and a very careful means of proceeding despite such concerns while ensuring those concerns are understood and recorded and considered.

What purpose does restart serve?

Sometimes, a proposal has changed enough to warrant re-checking people's positions, but has not changed enough to warrant closing it and starting the process over. restart clears people's statuses to ensure that they have the opportunity to re-confirm (or raise a hold or concern) before the decision proceeds.

Becoming and being a lang-team member

Lang team members are the ones who ultimately decide the syntax/semantics of the Rust language. They judge whether a given piece of Rust syntax ought to compile and, if it does, what should happen when it runs.

Relations to other teams

Lang team members should be familiar with how the compiler works, but they don't need to drive its implementation or even work on it. Compiler implementation details are the job of the compiler team. Note that compiler team members are encouraged to raise concerns if they feel that the desired design cannot be implemented in a reasoable and maintainable way.

Lang team members have to know the language inside and out, but they don't necessarily need deep type theory background. Working out the formal semantics of the language as well as diving into very detailed questions is covered by the types team. Types team members are encouraged to raise concerns if they feel that the desired design cannot be made sound or is internally inconsistent.

Expectations

Lang team members are expected to

  • Help to advance the state of the language by some combination of participating in discussions on RFCs and Zulip, authoring and editing RFCs, and shepherding features through the stabilization process.
  • Attend triage and design meetings regularly.
  • Promptly respond to RFC decisions by reviewing the question and either checking box or raising concerns

Process to add a new member

Lang team members can propose new additions to the team as follows:

  • Lang team member prepares a short write-up to propose candidate to the rest of the team
    • The write-up should draw on the qualifications below, giving examples where the candidate demonstrated the various criteria
  • Leads check with moderation team for known flags, surface to team if any.
  • The decision to add must have unanimous consent, similar to any other decision. Objections may be raised in a private team discussion, or by contacting a team lead.

Important: Discussions about potential new members are kept strictly confidential. Email or voice conversation are often preferred because they doesn't keep publicly available records.

Qualifications

These are the questions we ask ourselves when deciding whether someone would be a good choice as a lang team member.

  • Has this person demonstrated strong language design skills?
    • Have they led an impactful initiative to completion?
    • Have they identified a criticial flaw in someone else's design that led to the design being altered?
  • Is this person responsible?
    • When they agree to take on a task, do they either get it done or identify that they are not able to follow through and ask for help?
  • Is this person able to lead others to a productive conversation?
    • Are there times when a conversation was stalled out and this person was able to step in and get the design discussion back on track?
      • This could have been by suggesting a compromise, but it may also be by asking the right questions or encouraging the right tone.
  • Is this person able to disagree productively?
    • When they are having a debate, do they make an active effort to understand and repeat back others' points of view?
    • Do they "steelman", looking for ways to restate others' points in the most convincing way?
  • Is this person active?
    • Are they attending the triage/design meetings regularly?
    • Either in meeting or elsewhere, do they comment on disussions and otherwise?
  • Does this person have an overall desire to improve the language, rather than a strong interest in some particular domain?
    • Everyone have preferences, but members are responsible for balancing a wide array of interests. Someone with very specialized interest may be a better choice for a lang team advisor.

Lang team leadership

Lang team leads drive the overall team agenda and tend to its well-being. They generally chair the meetings, keep an eye out for conflicts, and are to some extent the decision makers of last resort. They drive the roadmap process. They also post blog posts updating the public (possibly through delegation).

Process to select leads

We do not have a fixed point in time for selecting new leadership. However, when we opt to select new leadership, the process is as follows:

  • Current lang team leads send out an email or message seeking nominations
  • Lang team members (including the current leads) suggest candidates for new leads
    • Members may nominate themselves.
    • Leads may nominate to continue leading.
  • Based on these nominations, leads propose the next set of leads and team members ratify the choice
    • A private meeting (lang team members only) may be useful to discuss.
    • It is generally recommended to pair one new co-lead with an older one who can advise.
    • To avoid burnout, rotation of leadership is recommended.

Qualifications

Questions to ask when nominating or selecting a lead:

  • Does this person have time to be a lang team lead?
  • Are they widely respected amongst the team?
  • Do they excel at resolving disputes, even amongst lang team members?
    • All leads are lang team members, so they are are assumed to meet the general criteria for lang team membership, but typically leads excel at the question of leading others to productive discussion.

Chat platform

The lang team hangs out in the rust-lang Zulip in the #t-lang stream. There are also other #t-lang/* streams that might be of interest.

Calendar

We have a lang-team calendar that shows the time for our various meetings. Meetings are generally open to anyone who wants to listen in. We also try to post minutes (and recordings, if any) on a "best-effort" basis from our meetings.

Meetings

The lang team has several kinds of standing meetings.

Unless otherwise noted, all of our meetings are open to the public for anyone to attend. You will find the timing and event details on our lang team calendar. We publish notes and minutes in written form in this github repository.

Recording policy

Dy default, our triage and design meetings are not recorded, in order to encourage engagement from a broad audience. We may record certain design meetings, evaluated on a case-by-case basis, and only with the agreement of all participants. Any intention to record a design meeting will be established at that time that meeting is scheduled during the monthly planning meeting and included in the blog post announcing the upcoming meetings.

Our YouTube playlist has recordings of some of our past meetings (along with automatically created subtitles).

Triage meeting

The weekly triage meeting is when we go over the newly filed project proposals along with issues that have been nominated for lang-team feedback. We also get regular updates from the active project groups so we can stay on top of what is going on.

Can I attend?

Yes! The triage meeting is open to the public. You'll find the details on our calendar.

How do I get something on the agenda?

The easiest way to get something on the agenda is to nominate it. The agenda is automatically built by triagebot from this template. We review pending initiative proposals, nominated issues and PRs from a variety of repositories, as well as pending RFC requests.

Can I generate the agenda myself?

Sure. You can visit the following link and one will be generated for you for (the current date):

https://triage.rust-lang.org/agenda/lang/triage

Alternatively, clone the triagebot repo and run this

> cargo run --bin lang agenda

If you install the hackmd-cli, you can do this:

cargo run --bin lang agenda | hackmd-cli import 

Where can I find the minutes?

Triage meeting minutes are available in this directory.

Design meetings

We reserve a weekly slot for our planning and design meetings. A design meeting is a one-hour deep-dive discussion into some particular topic. Each meeting is centered around a document that is prepared for that meeting explaining the details of what is to be discussed; we begin by reading the document and then discussing its contents. These meetings are used for all kinds of purposes, such as brainstorming, getting feedback on an idea, or building consensus around a specific proposals.

How are design meetings scheduled?

To schedule design meetings, we hold a special planning meeeting once per month. In that meeting, we choose what design meetings we will hold the rest of the month.

To generate the agenda for the planning meeting, you can use the following link and then copy/paste the generated text into a fresh hackmd page:

https://triage.rust-lang.org/agenda/lang/planning

How do I propose a design meeting?

You need to open an issue, as described here.

Can I attend?

Yes! Design meetings are open to the public. You'll find the details on our calendar.

How does a design meeting work?

Before the meeting starts, someone has to prepare a document -- we recommend using hackmd and using this template.

When the meeting starts, send out the link to your document on Zulip (and on Zoom, if you like). Everyone will start to read it. There is no expectation that people will read the document in advance.

As they read, people will append questions to the end of the document -- the template has a space for this. We recommend making each question into a markdown section (e.g., ### Why is this document so great?). People will append their question in that section.

After everyone is done reading, whoever is driving the meeting will pick questions to discuss. Typically we go in linear order but that's not required, we can go in whatever order seems best.

Where can I find the minutes?

The design-meeting-minutes directory contains the document from each meeting along with any questions that were asked and the ensuing disceussion.

Backlog bonanza

Backlog bonanza is a particular kind of design meeting. We often schedule a backlog bonanza for those weeks where we don't have more specific things to discuss. The idea is to go through each tracking issue and "disposition them". The goal is to identify what we ought to do with this particular unstable feature; e.g., what is blocking this from being stabilized? Do we still want this? Is it perma-unstable?

When does backlog bonanza take place?

Backlog bonanza meetings are typically scheduled as design meetings.

Can I attend?

Yes! Design meetings are open to the public. You'll find the details on our calendar.

What labels do we apply to issues?

Here are the labels we apply during the process and their meaning:

  • S-tracking-ready-to-stabilize: Needs a stabilization PR (good to go :train:)
  • S-tracking-needs-to-bake: Needs time to bake (set a date? other criteria?)
  • S-tracking-impl-incomplete: Not code complete or blocking bugs
  • S-tracking-unimplemented: Implementation not begun
  • S-tracking-design-concerns: Blocking design concerns
    • This might be "doesn't quite seem to deliver value we hoped for" or "something doesn't feel right"
  • S-tracking-perma-unstable
    • Internal implementation detail of rustc, stdlib
  • S-tracking-needs-investigation

Where can I find the minutes?

Currently the minutes are tracked in the issues themselves, but we also create hackmd documents in the Rust lang team.

This section contains "notes" about the design of various proposals. These are often just links to conversations, along with a few key ideas and summaries. Sometimes it includes other information, such as lang-team decisions about whether a particular proposal is viable.

Can we allow integer literals like 1 to be inferred to floating point type?

Background

In rust today, an integer like 1 cannot be inferred to floating point type. This means that valid-looking numeric expressions like 22.5 + 1 will not compile, and one must instead write 22.5 + 1.0. Can/should we change this?

History

This was discussed on Zulip in May 2020. Some of the key highlights from the discussion were:

Generalized coroutines

Since even before Rust 1.0, users have desired the ability to yield like in other languages. The compiler infrastructure to achieve this, along with an unstable syntax, have existed for a while now. But despite a lot of debate, we've failed to polish the feature up enough to stabilize it. I've tried to write up a summary of the different design considerations and the past debate around them below:

Terminology

  • The distinction between a "coroutine" and a "generator" can be a bit vague, varying from one discussion to the next.
  • In these notes a generator is anything which directly implements Iterator or Stream while a coroutine is anything which can take arbitrary input, yield arbitrary output, and later resume execution at the previous yield.
  • Thus, the "generator" syntax proposed in eRFC-2033 and currently implemented behind the "generator" feature is actually a coroutine syntax for the sake of these notes, not a true generator.
    • RFC-2996 defines a true generator syntax in "future additions".
  • Note also that "coroutines" here are really "semicoroutines" since they can only yield back to their caller.
  • I will continue to group the original eRFC text and the later generator resume arguments extension togther as "eRFC-2033". That way I only have 3 big proposals to deal with.
  • In rustc, a coroutine's "witness" is the space where stack-allocated values are stored if needed across yields. I'm borrowing this terminology here. Any such cross-yield bindings are said to be "witnessed".
// This is an example coroutine which might assist a streaming base64 encoder
|sextet, octets| {
    let a = sextet; // witness a, b, and c sextets for later use
    yield;
    let b = sextet;
    octets.push(a << 2 | b >> 4); // aaaaaabb
    yield;
    let c = sextet;
    octets.push((b & 0b1111) << 4 | c >> 2); // bbbbcccc
    yield;
    octets.push((c & 0b11) << 6 | sextet) // ccdddddd
}

// This is an example generator which might be used in Iterator::flat_map.
gen {
  for item in inner {
    for mapped in func(item) {
      yield mapped;
    }
  }
}

// This is an example async generator which might be used in Stream::and_then
async gen {
  while let Some(item) = inner.next().await {
    yield func(item).await;
  }
}

Coroutine trait

  • The coroutine syntax must produce implementations of some trait.
  • RFC-2781 and eRFC-2033 propose the Generator trait.
  • Note that Rust's coroutines and subroutines look the same from the outside: take input, mutate state, produce output.
  • Thus, MCP-49 proposes using the Fn* traits instead, including a new FnPin for immovable coroutines.
    • Hierarchy: Fn is FnMut + Unpin is FnPin is FnOnce.
      • May not be required at the trait level (someone may someday find a use to implementing FnMut + !FnPin) but all closures implement the traits in this order.

Coroutine syntax

  • The closure syntax is reused for coroutines by eRFC-2033, RFC-2781, and MCP-49.
  • Commentators have suggested that the differences between coroutines and closures under eRFC-2033 and RFC-2781 justify an entirely distinct syntax to reduce confusion.
  • MCP-49 fully reuses the semantics of closures, greatly simplifying the design space and making the shared syntax obvious.

Taking input

  • The major disagreement between past proposals is whether to use "yield expressions" or "magic mutation".
    • Yield expression: let after = yield output;
    • Magic mutation: let before = arg; yield output; let after = arg;
  • Many people have a strong gut preference for yield expressions.
    • In simple cases, Rust generally prefers to produce values as output from expressions rather than by mutation of state. "Yield expressions feel more Rusty."
    • However, magic mutation is likely correct, even though at first glance it feels surprising. In addition to reasons below, holding references to past resume args is rare, often a logic error. Rust can use mutation checks to catch and give feedback.
  • "Magic mutation" is a bit of a misnomer. The resume argument values are not themselves being mutated. The argument bindings are simply being reassigned across yields.
    • In a sense, argument bindings are reassigned in the exact same way across returns.
    • Previous arguments (if unmoved) are dropped prior to yielding and are reassigned after resuming.
    • People will get yelled at by the borrow checker if they try to hold borrows of arguments across yields. But the fix is generally easy: move the argument to a new binding before yielding.
=> |x| {
    let y = &x;
    yield;
    dbg!(y, x);
}

error[E0506]: cannot pass new `x` because it is borrowed
 --> src/lib.rs:3:4
  |
2 |     let y = &x;
  |             -- borrow of `x` occurs here
3 |     yield;
  |     ^^^^^ assignment to borrowed `x` occurs here
4 |     dbg!(y, x);
  |             - borrow later used here
  |
  = help: consider moving `x` into a new binding before borrowing

=> |x| {
    let a = x;
    let y = &a;
    yield;
    dbg!(y, x);
}
  • Magic mutation could be replaced by "magic shadowing" where new arguments shadow old ones at yield in order to allow easy borrowing of past argument values. But this is a huge footgun. See if you can spot the issue with the following code if ctx shadows its past value rather than overwriting it:
std::future::from_fn(|ctx| {
  if is_blocked() {
    register_waker(ctx);
    yield Pending;
  }

  while let Pending = task.poll(ctx) { .. }
})
  • "Yield expression" causes problems with first-resume input.
    • eRFC-2033 passes the first resume argument via a closure parameters while later arguments are produced by yield expressions.
    • This part of why it is so hard to unify generalized coroutines with a generator syntax like gen { } or gen fn. Where does the first input go? Where do you annotate the argument type even?
  • To increase clarity, users almost always want resume arguments to be named.
    • With magic mutation, all resume arguments are already named since they reuse the closures arguments on every resume. Any unmoved arguments are dropped just prior to yielding, so they are not witnessed and do not increase the coroutine size.
      • Also get multiple arguments for free if using the Fn* traits.
    • Yield expressions require users to repeatedly assign resume arguments to named bindings manually. Such bindings must be included in the closure state if they have any drop logic.

Borrowed resume arguments

  • What happens when a coroutine witnesses a borrow passed as a resume argument? For example:
let co = |x: &i32| {
  let mut values = Vec::new();
  loop {
    values.push(x);
    yield;
  }
};

// potentially ok:
let mut x = 0;
co(&x);

// must not be allowed:
x = 1;
co(&x);
  • As of writing, RFC-2781 leaves this as an unresolved question with a note to potentially restrict resume arguments to being 'static.
  • Since coroutines under MCP-49 act as much like closures as possible, and treat the witness and capture data the same whenever possible, the example above would fail in a similar way to the example below, giving a "borrowed data escapes into closure state" error or similar even if x is not mutated.
let mut values = Vec::new();
|x: &i32| {
  loop {
    values.push(x);
//  ^^^^^^^^^^^^^^ `x` escapes the closure body here
    yield;
  }
}
  • As of writing, eRFC-2033 appears to take a similar approach (although the error message is not super descriptive).
  • Ideally someday we'd do something nicer but any such solution would apply to both captured state and witnessed state in the same way.

Lending

  • Coroutines would eventually like to yield borrows of state to the caller. This is "lending" coroutine (sometimes also called an "attached" coroutine).
  • Using MCP-49, a lending coroutine might look like:
|| {
  let mut buffer = Vec::new();
  loop {
    let n = fill_buffer(&mut buffer);
    yield &buffer[..n];
  }
}
  • None of the major proposals have made an effort to resolve this directly as far as I am aware.
    • RFC-2996 gets the closest with a mention of LendingStream and LendingIterator traits in "future additions".
    • We should probably get some experience with lending traits at the lib level before attempting to add language level support.
  • If lending closures were implemented, MCP-49 could immediately be used to build lending streams, iterators, etc so long as the respective traits have the needed GAT-ification.

Enum-wrapping

  • RFC-2781 and eRFC-2033 propose that yield x should produce GeneratorState::Yielded(x) or equivalent as an output, in order to discriminate between yielded and returned values.
  • MCP-49 instead gives yield x and return x nearly identical semantics and output x directly, so the two must return the same type.
  • Enum-wrapping here is analogous to Ok-wrapping elsewhere. Similar debates result.
  • When using enum-wrapping, the syntax to specify distinct return/yield types is hotly debated.
  • Generators always want return and yield to have different types (() vs T) but a generator syntax on top of coroutines could be used to auto-insert enum wrappers around yield vs return arguments.
  • Auto-enum-wrapping can slightly improve type safety in some cases where return should be treated specially to avoid bugs.
  • No-enum-wrapping when combined with the impl Fn* choice of trait, allow the coroutine syntax to be used directly with existing higher-order methods on iterator, stream, collection types, async traits, etc.
  • Note these two approaches are "isomorphic": a coroutine that returns GeneratorState<T, T> could be wrapped to return T by some sort of combinator and a coroutine that only returns T can have yield and return values manually wrapped in GeneratorState. This is just about ergonomics:
// Without enum wrapping:
std::iter::from_fn(|| {
  yield Some(1);
  yield Some(2);
  yield Some(3);
  None
}).map(|x| {
  yield -x;
  yield x;
});

// With enum wrapping:
std::iter::from_gen_fn(|| {
  yield 1;
  yield 2;
  yield 3;
}).map(unwrap_gen_state(|x| {
  yield -x;
  yield x;
}));

// Needed for un-enum-wrapping when not desired.
// Could be replaced by sufficiently fancy !-casting?
fn unwrap_gen_state<T>(f: impl FnMut() -> GeneratorState<T, !>) -> T { ... }
fn merge_gen_state<T>(f: impl FnMut() -> GeneratorState<T, T>) -> T { ... }

// With no wrapping + generators:
(gen {
  yield 1;
  yield 2;
  yield 3;
}).map(|x| {
  yield -x;
  yield x;
})

Movability

  • All proposals want movability/impl Unpin to be inferred.
    • If we forbid "borrowed data escaping into closure state", the inference rules should be relatively simple: witnessing any borrow triggers immovability.
      • Dead borrows should not be witnessed.
    • But exact inference rules may only be well understood after an attempt at implementation.
  • Soundness of pin_mut! is a little tricky but seems to be fine no matter what.
    • If the resulting mutable borrow is witnessed ⇒ coroutine is !Unpin because of inference rules
    • If the pinned data is !Unpin and is witnessed ⇒ coroutine is !Unpin because witness contains !Unpin data
    • Thus, if the coroutine can be moved after resume, any data stack-pinned (really witness-pinned) by pin_mut! is not referenced and is Unpin.
  • Until inference is solved, the static keyword can be used as a modifier.
// movable via inference
|| {
  let x = 4;
  let y = &x;
  dbg!(y);
  yield;
}

// guaranteed movable (pending inference)
static || {
  ...
}

// immovable
|| {
  let x = 4;
  let y = &x;
  yield;
  dbg!(y);
}

"Once" coroutines

  • A lot of coroutines destroy captured data when run.
  • These coroutines (notably futures) can be resumed many times but can only be run through "once".
  • In contrast to non-yield FnOnce closures, this can not be solved at the type level because a coroutine can run out after an arbitrary, runtime-dependent number of resumptions.
    • Attempts to discriminate with enums tend to run up against Pin.
  • Coroutines must have the ability to block restart with a panic!.
    • Following return.
    • Following panic! and recovery.
    • The term "poison state" technically refers to only the later case. But here I will use it to mean any state at which the closure panics if resumed.
  • RFC-2781 and eRFC-2033 propose that all coroutines become poisoned after returning.
  • MCP-49 recommends that all non-capture-destroying coroutines resume at their initial state after returning.
    • This can be very handy in some situations. In fact, I use it several times in examples to increase readability. See anywhere I iter.map(coroutine) or the base64 encoder.
    • Similar question around generators: should they loop to save on a state or should they be fused-by-default?
    • If we do decide to panic-after-return, restart-after-return can still be emulated using loop { .. } as the coroutine body instead of simply { .. }. This is even zero-cost because unreachable poison states are eliminated.
  • MCP-49 also optionally proposes that capture-destroying closures should only implement FnOnce unless explicitly annotated, even if they should apparently be resumable several times.
    • mut || { drop(capture); } is recommended as the modifier, to hint that an FnMut impl is being requested when the closure in question would otherwise impl only FnOnce.
    • But the behavior of this modifier is probably too obscure and requires too much explanation vs "closures always impl FnMut/FnPin if they contain yield".

Async coroutines

  • I am aware of no strong proposal for an async version of generalized coroutines although a fair amount of discussion has taken place.
    • In the context of MCP-49, how should async || { ... yield ...} be handled in the very long-term? Error right now.
  • Async coroutines don't make much sense because of resume arguments. Async functions are already coroutines which take an &mut Context as a resume argument. How should additional arguments should be specified?
    • Do the additional args need to be passed every single poll or are they only needed when resuming after Ready?
    • If they are stored between Readys, how does that interact with the ban on witnessing external borrows? Badly.
    • On resume, the the coroutine might only take the additional arguments. It could then yield a future to take the async context and handle any Pending yields.
    • If so, how is the coroutine body broken up into distinct futures to be yielded?
    • What happens if a yielded future is destroyed early? Panic on resume?
  • Generators and async are both sugars on top of coroutines and are orthogonal to each other. But neither is orthogonal to the underlying coroutine feature:
// an async block
async {
  "hello"
}

// an async generator
async gen {
  yield "hello";
}

// an "async coroutine"
|ctx: &mut Context| {
  yield Poll::Ready("hello");
}
  • Taking the async context explicitly makes it cleaner to implement some complex async functions which take additional poll parameters.
    • An await_with! macro would be quite useful for implementing await loops on arbitrary Poll-returning functions.
      • Would be a good candidate for an .await(args..) syntax if very heavily used.
    • For example, an simple little checksumming async write wrapper might look like this:
|ctx: &mut Context, bytes: &[u8]| -> Poll<usize> {
  let mut checksum = 0;
  let mut count = 0;
  pin_mut!(writer);

  loop {
    let n = 4096 - count;
    if n == 0 {
      await_with!(writer.poll_write, ctx, &[checksum]);
    }

    let part = &bytes[..bytes.len().min(n)];
    checksum = part.fold(checksum, |x, &y| x ^ y);
    await_with!(writer.poll_write, ctx, part);

    count += part.len();
    yield Ready(part.len());
  }
}

Try

  • All proposals work fine with the ? operator without even trying (haha).
  • Poll<Result<_, _>> and Poll<Option<Result<_, _>>> already implement Try!
  • Generators usually want a totally different ? desugar that does yield Some(Err(...)); return None; instead of return Err(...).
    • This comes up a lot in discussions of general coroutine syntaxes but just muddies things up because (say it with me) generators ≠ coroutines.
    • Sugar-free implementation is easy: yield Some(try { ... }); None
  • Try blocks in general are super useful for handing errors by moving into specific error-handeling states.

Language similarity

  • Rust's version of coroutines can be a bit unusual compared to other languages. But the reason for this is simple: you need arguments to resume Rusty coroutines.
  • Resume arguments in other languages can be passed just fine by sharing mutable data. So all they need to implement are generators, not true coroutines as defined here.
# Generator function takes a word list and name on construction.
# The shared list is mutated to make room for new words.
def write_greeting(name, words):
    words.append('hello')
    if words.is_full:
        yield
    words.append(name)

// Function only needs name to construct coroutine.
// Coroutine gets mutable access to the word list each resume.
fn write_greeting(name: String) -> impl FnMut(&mut Vec<String>) {
  |words| {
    words.push("hello".to_string());
    if words.len() == words.capacity() {
      yield
    }
    words.push(name);
  }
}

Language complexity

  • The main selling point of MCP-49 is that it avoids adding a whole new language feature with associated design questions. Instead, the common answer to questions regarding MCP-49 is that yield-closures simply do whatever closures do.
    • The syntax is the same.
    • Captures work the same way.
    • Arguments are passed the same way.
    • Return and yield both drop the latest arguments and then pop the call stack.
    • The only big difference is that once yield is involved, some variables get stored in a witness struct rather than in the stack frame. Plus the need for a poison state.
  • In fact, return behaves exactly like a simultaneous yield + break 'closure_body.
    • In a sense, every closure already has a single yield point at which it resumes after return.
    • A yield adds a second resume point: hence the need for a discriminant.
  • Under that proposal, anywhere a closure can be used, a coroutine can too. And vice versa.

Generator unification

  • So far in this proposal, I've been very careful to distinguish generators (as supported by the propane and async_stream crates, proposed by RFC-2996, etc) from the coroutines discussed here. They are treated as two separate language features.
  • Does Rust have "room" for both stream syntax and a generator syntax? Would it be better to find a single solution to both?
  • A single solution is difficult for a few reasons:
    • Taking resume arguments muddies the syntax. For example, what would be the syntax for a generator function which takes an explicit resume argument?
    • The closure syntax works great for coroutines which implement Fn* a la MCP-49! But reusing that syntax to magically implement Iterator or Stream would cause confusion.
    • On that note, generators definitely want to implement different traits vs coroutines. Iterator and Stream rather than Fn or (ironically) Generator.
    • As stated above, async coroutines don't make much sense: async interacts poorly with resume arguments.
    • Async generators are super important, don't care about resume arguments.
    • As mentioned in the section on try, generators and coroutines generally want different error handling. Or at lest, some more complex ? desugar is not so obvious for coroutines in general as it is for generators specifically.
  • Once generalized coroutines are in place, a generator syntax like the one in RFC-2996 is a trivial sugar on top:
gen {
  for item in inner {
    for mapped in func(item) {
      yield mapped;
    }
  }
}

// becomes

std::iter::from_fn(|| {
  for item in inner {
    for mapped in func(item) {
      yield Some(mapped);
    }
  }
  None
})
async gen {
  while let Some(item) = inner.next().await {
    yield func(item).await;
  }
}

// becomes

std::stream::from_fn(|ctx| {
  while let Some(item) = await_with!(inner.next(), ctx) {
    yield Ready(Some(await_with!(func(item), ctx)));
  }
  Ready(None)
})
  • Proc-macro crates could provide very satisfactory gen and gen_async macros until we are sure of the need to support such a sugar directly in language as a keyword or in core as a first-party macro.

Past discussions

There are a lot of these. Dozens of internals threads, reddit posts, blog posts, draft RFCs, pre RFCs, actual RFCs, who knows what in Zulip, and so on. So this isn't remotely exhaustive:

  • https://github.com/CAD97/rust-rfcs/pull/1
  • https://github.com/rust-lang/lang-team/issues/49
  • https://github.com/rust-lang/rfcs/pull/2033
  • https://github.com/rust-lang/rfcs/pull/2781
  • https://github.com/rust-lang/rust/issues/43122
  • https://github.com/rust-lang/rust/pull/68524
  • https://internals.rust-lang.org/t/crazy-idea-coroutine-closures/1576
  • https://internals.rust-lang.org/t/no-return-for-generators/11138
  • https://internals.rust-lang.org/t/syntax-for-generators-with-resume-arguments/11456
  • https://internals.rust-lang.org/t/trait-generator-vs-trait-fnpin/10411
  • https://reddit.com/r/rust/comments/dvd3az/generalizing_coroutines/
  • https://samsartor.com/coroutines-2
  • https://smallcultfollowing.com/babysteps/blog/2020/03/10/async-interview-7-withoutboats/#async-fn-are-implemented-using-a-more-general-generator-mechanism
  • https://users.rust-lang.org/t/coroutines-and-rust/9058

Extending the capabilities of compiler-generated function types

Background

Both standalone functions and closures have unique compiler-generated types. The rest of this document will refer to both categories as simply "function types", and will use the phrase "function types without upvars" to refer to standalone functions and closures without upvars.

Today, these function types have a small set of capabilities, which are exposed via trait implementations and implicit conversions.

  • The Fn, FnMut and FnOnce traits are implemented based on the way in which upvars are used.

  • Copy and Clone traits are implemented when all upvars implement the same trait (trivially true for function types without upvars).

  • auto traits are implemented when all upvars implement the same trait.

  • Function types without upvars have an implicit conversion to the corresponding function pointer type.

Motivation

There are several cases where it is necessary to write a trampoline. A trampoline is a (usually short) generic function that is used to adapt another function in some way.

Trampolines have the caveat that they must be standalone functions. They cannot capture any environment, as it is often necessary to convert them into a function pointer.

Trampolines are most commonly used by compilers themselves. For example, when a dyn Trait method is called, the corresponding vtable pointer might refer to a trampoline rather than the original method in order to first down-cast the self type to a concrete type.

However, trampolines can also be useful in low-level code that needs to interface with C libraries, or even in higher level libraries that can use trampolines in order to simplify their public-facing API without incurring a performance penalty.

By expanding the capabilities of compiler-generated function types it would be possible to write trampolines using only safe code.

Purpose

The goal of this design note is describe a range of techniques for implementing trampolines (defined below) and some of the feedback regarding those solutions. This design note does not intend to favor any specific solutions, just reflect past discussions. The presence or absence of any particular feedback in this document does not necessarily serve to favor or disfavor any particular solution.

History

Several mechanisms have been proposed to allow trampolines to be written in safe code. These have been discussed at length in the following places.

PR adding Default implementation to function types:

  • https://github.com/rust-lang/rust/pull/77688

Lang team triage meeting discussions:

  • https://youtu.be/NDeAH3woda8?t=2224
  • https://youtu.be/64_cy5BayLo?t=2028
  • https://youtu.be/t3-tF6cRZWw?t=1186

Example

An adaptor which prevents unwinding into C code

In this example, we are building a crate which provies a safe wrapper around an underlying C API. The C API contains at least one function which accepts a function pointer to be used as a callback:

#![allow(unused)]
fn main() {
mod c_api {
    extern {
        pub fn call_me_back(f: extern "C" fn());
    }
}
}

We would like to allow users of our crate to safely use their own callbacks. The problem is that if the callback panics, we would unwind into C code and this would be undefined behaviour.

To avoid this, we would like to interpose between the user-provided callback and the C API, by wrapping it in a call to catch_unwind. Unfortunately, the C API offers no way to pass an additional "custom data" field that we could use to store the original function pointer.

Instead, we could write a generic function like this:

#![allow(unused)]
fn main() {
use std::{panic, process};

pub fn call_me_back_safely<F: Fn() + Default>(_f: F) {
    extern "C" fn catch_unwind_wrapper<F: Fn() + Default>() {
        if panic::catch_unwind(|| {
            let f = F::default();
            f()
        }).is_err() {
            process::abort();
        }
    }
    unsafe {
        c_api::call_me_back(catch_unwind_wrapper::<F>);
    }
}
}

This compiles, and is intended to be used like so:

fn my_callback() {
    println!("I was called!")
}

fn main() {
    call_me_back_safely(my_callback);
}

However, this will fail to compile with the following error:

error[E0277]: the trait bound fn() {my_callback}: Default is not satisfied

Implementing the Default trait

The solution initially proposed was to implement Default for function types without upvars. Safe trampolines would be written like so:

#![allow(unused)]
fn main() {
fn add_one_adapter<F: Fn(i32) + Default>(arg: i32) {
    let f = F::default();
    f(arg + 1);
}
}

Discussions of this design had a few central themes.

When should Default be implemented?

Unlike Clone, it intuitively does not make sense for a closure to implement Default just because its upvars are themselves Default. A closure like the following might not expect to ever observe an ID of zero:

#![allow(unused)]
fn main() {
fn do_thing() -> impl FnOnce() {
    let id: i32 = generate_id();
    || {
      do_something_with_id(id)
    }
}
}

The closure may have certain pre-conditions on its upvars that are violated by code using the Default implementation. That said, if a function type has no upvars, then there are no pre-conditions to be violated.

The general consensus was that if function types are to implement Default, it should only be for those without upvars.

However, this point was also used as an argument against implementing Default: traits like Clone are implemented structurally based on the upvars, whereas this would be a deviation from that norm.

Leaking details / weakening privacy concerns

Anyone who can observe a function type, and can also make use of the Default bound, would be able to safely call that function. The concern is that this may go against the intention of the function author, who did not explicitly opt-in to the Default trait implementation for their function type.

Points against this argument:

  • We already leak this kind of capability with the Clone trait implementation. A function author may write a FnOnce closure and rely on it only being callable once. However, if the upvars are all Clone then the function itself can be cloned and called multiple times.

  • It is difficult to construct practical examples of this happening. The leakage happens in the wrong direction (upstream) to be easily exploited whereas we usually care about what is public to downstream crates.

    Without specialization, the Default bound would have to be explicitly listed which would then be readily visible to consumers of the upstream code.

  • Features like impl Trait make it relatively easy to avoid leaking this capability when it's not wanted.

Points for this argument:

  • The Clone trait requires an existing instance of the function in order to be exploited. The fact that the Default trait gives this capability to types directly makes it sufficiently different from Clone to warrant a different decision.

These discussions also raise the question of whether the Clone trait itself should be implemented automatically. It is convenient, but it leaves a very grey area concerning which traits ought to be implemented for compiler-generated types, and the most conservative option would be to require an opt-in for all traits beyond the basic Fn traits (in the case of function types).

Unnatural-ness of using Default trait

Several people objected on the grounds that Default was the wrong trait, or that the resulting code seemed unnatural or confusing. This lead to proposals involving other traits which will be described in their own sections.

  • Some people do not see Default as being equivalent to the default-constructible concept from C++, and instead see it as something more specialized.

    To avoid putting words in people's mouths I'll quote @Mark-Simulacrum directly:

    I think the main reason I'm not a fan of adding a Default impl here is because you (probably) would never actually use it really as a "default"; e.g. Vec::resize'ing with it is super unlikely. It's also not really a Default but more just "the only value." Certainly the error message telling me that Default is not implemented for &fn() {foo} is likely to be pretty confusing since that does have a natural default too, like any pointer to ZST). That's in some sense just more broadly true though.

  • There were objections on the grounds that Default is not sufficient to guarantee uniqueness of the function value. Code could be written today that exposes a public API with a Default + Fn() bound, expecting all types meeting that bound to have a single unique value.

    If we expanded the set of types which could implement Default + Fn() (such as by stabilizing Fn trait implementations or by making more function types implement Default) then the assumptions of such code would be broken.

    On the other hand, we really can't stop people from writing faulty code and this does not seem like a footgun people are going to accidentally use, in part because it's so obscure.

New lang-item

This was a relatively minor consideration, but it is worth noting that this solution would require making Default a lang item.

Safe transmute

This proposal was to materialize the closure using the machinery being added with the "safe transmute" RFC to transmute from the unit () type.

The details of how this would work in practice were not discussed in detail, but there were some salient points:

  • This solves the "uniqueness" problem, in that ZSTs are by definition unique.

  • It does not help with the "privacy leakage" concerns.

  • It opens up a new can of worms relating to the fact that ZST closure types may still have upvars.

  • Several people expressed something along the lines of:

    if we were going to have a trait that allows this, it might as well be Default, because telling people "no, you need the special default" doesn't really help anything.

    Or, that if it's possible to do this one way with safe code, it should be possible to do it in every way that makes sense.

Singleton or ZST trait

New traits were proposed to avoid using Default to materialize the function values. The considerations here are mostly the same as for the "safe transmute" propsal. One note is that if we were to add a Singleton trait, it would probably make sense for that trait to inherit from the Default trait anyway, and so a Default implementation now would be backwards-compatible.

FnStatic trait

This would be a new addition to the set of Fn traits which would allow calling the function without any self argument at all. As the most restrictive (for the callee) and least restrictive (for the caller) it would sit at the bottom of the Fn trait hierarchy and inherit from Fn.

  • Would be easy to understand for users already familiar with the Fn trait hierarchy.
  • More unambiguously describes a closure with no upvars rather than one which is a ZST.
  • Doesn't solve the problem of accidentally leaking capabilities.
  • Does not force a decision on whether closures should implement Default.

This approach would also generalize the existing closure -> function pointer conversion for closures which have no upvars. Instead of being special-cased in the compiler, the conversion can apply to all types implementing FnStatic. Furthermore, the conversion could be implemented by simply returning a pointer to the FnStatic::call_static function, which makes this very elegant.

Example

With this trait, we can implement call_me_back_safely from the prior example like this:

#![allow(unused)]
fn main() {
use std::{panic, process};

pub fn call_me_back_safely<F: FnStatic()>(_f: F) {
    extern "C" fn catch_unwind_wrapper<F: FnStatic()>() {
        if panic::catch_unwind(F::call_static).is_err() {
            process::abort();
        }
    }
    unsafe {
        c_api::call_me_back(catch_unwind_wrapper::<F>);
    }
}
}

Const-eval

Initially proposed by @scalexm, this solution uses the existing implicit conversion from function types to function pointers, but in a const-eval context:

#![allow(unused)]
fn main() {
fn add_one_adapter<const F: fn(i32)>(arg: i32) {
    F(arg + 1);
}

fn get_adapted_function_ptr<const F: fn(i32)>() -> fn(i32) {
    add_one_adapter::<F>
}
}
  • Avoids many of the pitfalls with implementing Default.
  • Requires giving up the original function type. There could be cases where you still need the original type but the conversion to function pointer is irreversible.
  • It's not yet clear if const-evaluation will be extended to support this use-case.
  • Const evaluation has its own complexities, and given that we already have unique function types, it seems like the original problem should be solvable using the tools we already have available.

Opt-in trait implementations

This was barely touched on during the discussions, but one option would be to have traits be opted-in via a #[derive(...)]-like attribute on functions and closures.

  • Gives a lot of power to the user.
  • Quite verbose.
  • Solves the problem of leaking capabilities.

Auto traits

Auto traits permit automatically implementing a trait for types which contain fields implementing the trait. That is, they are fairly close to an automatic derive. They describe properties of types rather than behaviors; current stable Rust has several auto traits: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe.

Freeze is also an auto trait indirectly observable on stable; it is used by the compiler to determine which types can be placed in read-only memory, for example.

Auto traits are tracked in rust-lang/rust#13231, and are also sometimes referred to as OIBITs ("opt-in built-in traits").

As of November 2020, the language team feels that new auto traits are unlikely to be added or stabilized. See discussion on the addition of Freeze for context. There is a fairly high burden to doing so on the ecosystem, as it becomes a concern of every library author whether to implement the trait or not.

Each auto trait represents a semver compatibility hazard for Rust libraries, as adding private fields can remove the auto trait unintentionally from a type.

Stabilizing the ability to define auto traits also allows "testing" for the absence of a specific type:

auto trait NoString {}
impl !NoString for String {}

This is not something we generally want to allow, as it makes almost any change to types semver breaking. That means that stabilizing defining new auto traits is currently unlikely.

Eager drop design note

Observations

Any attempt to make drop run more eagerly will have to take borrows into account

The original proposal was to use "CFG dead" as a criteria, but it's pretty clear that this will not work well. Example:

#![allow(unused)]
fn main() {
{
    let x = String::new();
    let y = &x;
    // last use of x is here
    println!("{}", y);
    // but we use y here
    ...
}
}

Here, the fact that y (indirectly) uses x feels like an important thing to take into account.

Some destructors can be run "at any time"...

Some destructors have very significant side-effects. The most notable example is dropping a lock guard.

Others correspond solely to "releasing resources": freeing memory is the most common example, but another might be replacing an entry in a table because you are done using it.

...but sometimes that significance is only known at the call site

However, it can be hard to know what is significant. For a lock guard, for example, if the lock is just being used to guard the data, then moving the lock release early is actually desirable, because you want to release the lock as soon as you are doing changing the data. But sometimes you have a Mutex<()>, in which case the lock has extra semantics. It's hard to know for sure.

Smarter drop placement will mean that adding uses of a variable changes when its destructor runs

This is not necesarily a problem, but it's an obvious implication: right now, the drop always runs when we exit the scope, so adding further uses to a variable has no effect, but that would have to change. That could be surprising (e.g., adding a debug printout changes the time when a lock is released).

In contrast, if you add an early drop drop(foo) today, you get helpful error messages when you try to use it again.

In other words, it's useful to have the destructor occurring at a known time (sometimes...).

Today's drop rules are, however, a source of confusion

The semantics of let _ = <expr> have been known to caught a lot of confusion, particularly given the interaction of place expressions and value expresssions:

  • let _ = foo -- no effect
  • let _ = foo() -- immediately drops the result of invoking foo()
  • let _guard = foo -- moves foo into _guard and drops at the end of the block
  • let _guard = foo() -- moves foo() into _guard and drops at the end of the block

Another common source of confusion is the lifetimes of temporaries in match statements and the like:

#![allow(unused)]
fn main() {
match foo.lock().data.copy_out() {
    ...
} // lock released here!
}

let guard = foo; ...; drop(guard); has the advantage of explicitness, so does something like foo.with(|guard| ...)

Clarity for unsafe code can be quite important

There are known footguns today with the timing of destructors and unsafe code. For example, CString::new().as_ptr() is a common thing people try to do that does not work. Eager destructors would enable more motion, which might exacerbate the problem.

In addition, unsafe code means we might not be able to know the semantics associated with a destructor, such as what precisely a Mutex<()> guards, and moving a drop earlier will break some unsafe code in hard-to-detect ways.

Alternatives

  • Scoped methods
  • let blocks
  • "Defer" type constructs or scoped guard type constructs from other languages
    • Go
    • D
    • Python
  • Built-in macros or RAII/closure-based helpers in the standard library.
    • Note that the scopeguard crate offers macros like defer! that inject a let into the block.

Autoref/Autoderef in operators

Rust permits overriding most of the operators (e.g., +, -, +=). In part due to Copy types not auto-dereferencing, it is common to have T + &T or &T + &T, with potentially many levels of indirection on either side of the operator.

There is desire in general to avoid needing to both add impls for referenced versions of types because they:

  • bloat documentation
  • are never quite sufficient (always more references are possible)
  • can cause inference regressions, as the compiler cannot in general know that &T + &T is essentially equivalent at runtime to T + T.

The inference regressions are the primary target of historical discussions.

The tradeoff to some feature like this may either mean that the exact impl executed at runtime is harder to determine, or that the compiler is synthetically generating new implementations for some subset of types, potentially adding confusion around which impls are actually present.

However, generic code may want the impls on references because the generic code may not want to require T: Copy. One version of this could involve something like default impl<T:Copy> Add<&T> for &T so that people don't need to write special impls themselves. It's worth noting that this still would need some special support in the compiler to avoid the two possible impls leading to inference regressions.

History

The standard library initially had just the basic impls of the operator traits (e.g., impl Add<u64> for u64) but has since gained &u64 + &u64, u64 + &u64 and &u64 + u64. These impls usually cause some amount of inference breakage in practice.

Especially with non-Copy types (for example bigints), forcing users to add references can be increasingly verbose: &u * &(&(&u.square() + &(&a * &u)) + &one), for example.

There have also been a number of discussions on RFCs and issues, including:

Copy type ergonomics

Background

There are a number of pain points with Copy types that the lang team is interested in exploring, though active experimentation is not currently ongoing.

Some key problems are:

Copy cannot be implemented with non-Copy members

There are standard library types where the lack of a Copy impl is an active pain point, e.g., MaybeUninit and UnsafeCell, when the contained type is actually Copy.

History

  • unsafe impl Copy for T which avoids the requirement that T is recursively Copy, but is obviously unsafe.
    • https://github.com/rust-lang/rust/issues/25053#issuecomment-218610508
  • Copy is dangerous on types like UnsafeCell where &UnsafeCell<T> otherwise would not permit access to T in safe code.

Copy types can be (unintentionally) copied

Even if a type is Copy (e.g., [u8; 1024]) it may not be a good idea to make use of that in practice, since copying large amounts of data is slow. This is primarily a performance concern, so the problem is usually that these copies are easy to miss. However, depending on the size of the buffer, it can also be a correctness concern as it may cause an unintended stack overflow with too many accidental copies.

Should we want to lint on this code, deciding on a size threshold may be difficult. It's not generally possible for the compiler to know whether a particular copy operation is likely to lead to stack overflow or undesirable performance. We don't have examples yet of cases where there's desirable large copies (that should not be linted against) or concrete cases where the copies are accidental; collecting this information would be worthwhile.

Implementations of Copy on closures and arrays are the prime example of Rust currently being overeager with the defaults in some contexts.

This also comes up with Copy impls on Range, which would generally be desirable but is error-prone given the Iterator/IntoIterator impls on ranges.

The example here does not compile today (since Range is not Copy), but would be unintuitive if it did.

#![allow(unused)]
fn main() {
let mut x = 0..10;
let mut c = move || x.next();
println!("{:?}", x.next()); // prints 0
println!("{:?}", c()); // prints 0, because the captured x is implicitly copied.
}

This example illustrates the range being copied into the closure, while the user may have expected the name "x" to refer to the same range in both cases.

The move keyword here likely disambiguates this particular case for users, but in closures with more captures it may be not as obvious that the range type in particular was copied in.

A lint has been proposed to permit Copy impls on types where Copy is likely not desirable with particular conditions (e.g., Copy of IntoIterator-implementing types after iteration).

Note that "large copies" comes up with moves as well (which are copies, just taking ownership as well), so a size-based lint is plausibly desirable for both.

History

References to Copy types

Frequently when dealing with code generic over T you end up needing things like [u8]::contains(&5) which is ugly and annoying. Iterators of copy types also produce &&u64 and similar constructs which can produce unexpected type errors.

#![allow(unused)]
fn main() {
for x in &vec![1, 2, 3, 4, 5, 6, 7] {
    process(*x); // <-- annoying that we need `*x`
}

fn process(x: i32) { }
}
#![allow(unused)]
fn main() {
fn sum_even(v: &[u32]) -> u32 {
    // **v is annoying
    v.iter().filter(|v| **v % 2 == 0).sum()
}
}

Note that this means that you in most cases want to "boil down" to the inner type when dealing with references, i.e., &&u32 you actually want u32, not &u32. Notably, though, this may not be true if the Copy type is something more complex (e.g., a future Copy Cell), since then &Cell is quite different from a Cell, the latter being likely useless for modification at least.

There is also plausibly performance left on the table with types like &&u64.

Note that this interacts with the unintentional copies (especially of large structures).

This could plausibly be done with moved values as well, so long as the semantics match the syntax (e.g. wants_ref(foo) acts like wants_ref(&{foo})) similar to how one can pass &mut to something that only wants &. This would be a tradeoff: in some cases people may want the type-checker to flag such cases and require explicitly taking a reference, while in other cases people may want the compiler to automatically make such code work. We would want to consider and evaluate this tradeoff, and whether we can usefully separate such cases.

History

This section contains lang-team roadmaps.

Lang Team Roadmap 2024

Rust 1.0 was released in 2015. Since that time, we've seen Rust grow from a small language used for a handful of prominent projects into a mainstay in use at virtually every major tech company.

As we work towards Rust 2024, it's natural to ask what's next for the language. This roadmap provides insight into that question by describing what we, as members of the lang team with input from other Rust teams, would like to prioritize.

We have two goals with this roadmap:

  • to give people a sense for what to expect in Rust over the next few years;
  • for those who would like to contribute to Rust, to help provide "starting points" for how to get involved, and a sense for what kind of projects we are looking for.

Rust 2024: scaling empowerment

Rust's goal is to empower everyone to build reliable and efficient software. Success requires not only designing and implementing a great language with great libraries and great tools, but also maintaining a great and supportive community.

Our focus for Rust 2024 is to scale empowerment in many different ways. As we grow, we face increasing challenges in how we can scale the ways in which we empower people to an increasing number of people. This roadmap presents three general themes we plan to focus on:

  • Flatten the (learning) curve: scaling to new users and new use cases
    • Make Rust more accessible to new and existing users alike, and make solving hard problems easier.
  • Help Rust's users help each other: scaling the ecosystem
    • Empower library authors so they can---in turn---empower their users.
  • Help the Rust project scale: scaling the project
    • Develop processes to scale to the needs and use cases of a growing number of users; evaluate and finish projects we've started.

For each theme, we'll describe our goals for Rust 2024, and give a few examples of the kinds of things that we're working on right now, as well as the kinds of things we would like to do over the next few years.

This roadmap is a starting point. Our intent is to highlight those areas that will have the biggest impact on Rust's success. Specific examples will change over time, whether because they're finished or because new proposals arise. As 2023 approaches, we will revisit these themes to see how much progress we have made, and whether we wish to adjust the list.

Theme: Flatten the (learning) curve

The vision

Thanks to a consistent focus on ergonomics, Rust has become considerably easier to use over the last few years. Companies building large teams of Rust users report that the typical onboarding time for a Rust engineer is around 3-6 months. Of course, once folks learn Rust, they typically love it. Even so, many people report a sense of high "cognitive overhead" in using it, and "learning curve" remains the most common reason not to use Rust. The fact is that, even after you learn how the Rust borrow checker works, there remain a lot of "small details" that you have to get just right to get your Rust program to compile.

For Rust 2024, we will identify and eliminate many of those patterns and idiosyncracies that one must learn to use Rust; our goal is to let you focus squarely on the "inherent complexity" of your problem domain and avoid "accidental complexity" from Rust as much as possible.

Async and embedded Rust are areas of particular interest. We have made a lot of strides to support those areas, and they are growing rapidly. Nonetheless, Rust lacks many core capabilities that would be required to make working in those domains not only possible but straightforward and delightful. For Rust 2024, we will close that gap.

Our plan for achieving this vision is to focus on four high-level goals (in order from broad to precise):

  • More precise analyses, less rigamarole: Make the compiler better able to recognize when code is correct via improvements to the borrow checker, type inference, and so forth. Identify and eliminate "boilerplate" patterns like having to copy-and-paste the same set of where clauses everywhere.
  • Express yourself more easily: Where necessary, extend the language so you can express what you want your code to do more directly. In some cases this takes the form of syntactic sugar (such as let-else) but in other cases it may mean extending the type system to be able to describe new patterns (such as generic associated types).
  • Improve async support: Extend our async-await support beyond the current "MVP" to include features like async fns in traits, async drop, and other features needed to support the async vision document roadmap.
  • Make dyn Trait more usable: Broaden the set of traits that can be used with dyn and make working with dyn closer to working with generics.

How you can help

Join the rust-lang Zulip, and either start a thread in the #t-lang/roadmap-2024 stream, or send a private message to nikomatsakis if you'd like to discuss privately first.

The plan (so far)

Current active initiatives in each category include:

  • More precise analyses, less rigamarole:
    • Non-lexical lifetimes were a big stride forward, but the polonius project promises to improve the borrow check's precision even more.
    • Implied bounds promise to remove a lot of copy-and-pasting of where clauses.
  • Express yourself more easily:
    • let-else directly express the "match this variant or return/continue/etc" pattern.
    • let-chains allow you to express iterative refinement with a series of pattern-matches and conditionals
    • "Type alias" impl Trait permits APIs to name previously unnameable types. This is part of a larger effort to expand impl Trait.
    • Generic associated types allow traits to express a number of patterns (like "iterable") that the current trait system cannot handle. They are a particularly important foundational piece for async programming.
  • Improve async support:
    • We are working to support async fns in traits, including both static dispatch and dyn dispatch.
  • Make dyn Trait more usable:

Looking forward

Looking beyond the initiatives that are in progress, there's a lot of room for more improvement. Here are some other ideas we'd like to see. For many of these ideas, the main thing they need is someone to own the design! If you might be interested in giving that a try, come to #t-lang/roadmap-2024 to discuss, or send a private message to nikomatsakis.

  • More precise analyses, less rigamarole:
    • Deref patterns: Permit matching types with patterns they can dereference to, such as matching a String with a "str".
    • Perfect derive: determine the precise conditions for generic type parameters based on the types of a struct fields. For instance, #[derive(Clone)] struct MyStruct(Rc<T>) would not require T: Clone, because Rc<T> can be cloned without it.
    • Autoref, operators, and clones: Generic methods that operate on references sometimes necessitate types like &u32; since u32 is Copy, we could automatically make it a reference. We've historically had some hesitance to add more reference-producing operations, because it can lead to types the user doesn't expect (such as &&&str). We have some ideas to simplify those cases and avoid unnecessary double-references.
  • Express yourself more easily:
    • Generators, allowing users to write iterators (async and otherwise) using custom syntax.
  • Improve async support:
    • After adding async fn in traits, we intend to add support for async drop, async closures, and potentially other features.
  • Make dyn Trait more usable:
    • Make more patterns "object safe" and thus usable in dyn Trait objects, including passing self by value and handling impl Trait in argument position (see this post for more information).

Theme: Help users help each other

The vision

Rust's combination of ownership and borrowing, low-level systems control, and powerful extensibility mechanisms like procedural macros makes it a great language for writing libraries. And, thanks to Cargo, using a library in your program only takes a few lines of code. Nonetheless, there are a number of things that library authors can't do, or can't do easily -- for example, they can't control the error messages you see or deploy an "unstable" feature that requires special opt-in to use. For Rust 2024, we want to build features that empower library authors to better serve their users, either by helping to manage the feature lifecycle or by expanding the capabilities of what a library can do.

We encourage people to experiment and explore in the library ecosystem, building new functionality for people to use. Sometimes, that new functionality becomes a foundation for others to build on, and standardizing it simplifies further development atop it, letting the cycle continue at another level. However, some aspects of the Rust language (notably coherence) make it harder to extend the Rust standard library or well-established crates from separate libraries, discouraging experimentation. Other features (such as aspects of method resolution) make it hard to promote best-in-class functionality into the standard library or into well-established crates without breaking users of the crates that first developed that functionality. For Rust 2024, we want to pursue changes that enable more exploration in the ecosystem, and enable stable migration of code from the ecosystem into the standard library.

Our plan for achieving this vision is to focus on four categories of work:

  • Feature lifecycle: Help library authors support features as they move from experimental to finalized. Help library authors manage their development lifecycle and evolution.
  • Richer abstractions: Extend the language to let library authors express richer abstractions.
  • Custom developer experience: Permit library authors can tailor the developer experience, for example by tailoring the error messages a user gets when traits are not implemented or introducing custom lints.
  • Interoperability: The library ecosystem can easily coordinate, making libraries work together without tying them together. Library authors can write code that is portable across many environments or specific to one, as they please.

How you can help

Join the rust-lang Zulip, and either start a thread in the #t-lang/roadmap-2024 stream, or send a private message to Josh Triplett if you'd like to discuss privately first.

The plan (so far)

Current active initiatives in each category include:

  • Feature lifecycle:
  • Richer abstractions:
  • Custom developer experience:
    • We are not currently doing any coordinated initiatives here, though there are ongoing efforts that help lay groundwork for this.
  • Interoperability:

Looking forward

Looking beyond the initiatives that are in progress, there's a lot of room for more improvement. Here are some other ideas we'd like to see. For many of these ideas, the main thing they need is someone to own the design! If you might be interested in giving that a try, come to #t-lang/roadmap-2024 to discuss, or send a private message to Josh Triplett.

  • Feature lifecycle:
    • All ecosystem crates can have "release trains", with the equivalent of "nightly features" that require a stability opt-ins. Top-level crates retain control over whether any of their dependencies may use nightly features.
  • Richer abstractions:
    • Allow libraries to implement the Fn traits to define callable objects.
    • Variadic tuples and variadic generics would address a common pain point of "implement this trait for tuples of any arity".
  • Custom developer experience:
    • Allow libraries to provide custom lints for their users.
    • Allow libraries to control or customize Rust diagnostics, especially for trait resolution failures.
  • Interoperability:
    • Revive the stalled portability lint or pursue an alternative design (a recent suggestion is that the "platform" might be a global service, similar to RFC 2492, permitting one to use where clauses to designate portable code)
    • The coherence rules make it hard to implement interoperability traits; we should find a way to lift this restriction, while preserving coherence's key benefits.
    • Adopt a standard way to write performance benchmarks (perhaps simply adopt criterion officially).
    • Better support for dynamic linking, with richer and safer types than the C ABI. For instance, implement an extern "safe" providing a subset of Rust's rich types.

Theme: Help the Rust project scale

The vision

The Rust repo is a blizzard of activity. This is great, but it can be overwhelming, particularly if you are trying to figure out the status of some particular thing that you are interested in or would like to contribute to.

To ship Rust 2024 and make Rust all that it can be, we need a system that makes it easy for people to find out what's going on and how they can help. We want to scale our language development through delegation, empowering developers to own and drive the work that they are passionate about. Lang team liaisons and frequent lang team check-in will help ensure quality, stability, and overall coherence. The team itself will have a clear "path to membership" that helps us to maintain our membership and make sure we have the expertise we need.

Our plan for achieving this vision is to focus on four categories of work:

  • See the status at a glance: We want it to be easy to identify what things the lang-team is actively working on and how far those designs have come. We want every tracking issue to clearly identify what "next steps" are needed to push that particular feature over the finish line and make sure that those steps are clearly documented for would-be contributors.
  • Clear owners and clear communication: Rust operates by consensus, but that doesn't mean that everybody has to know all the details of everything. We need a system that has clear owners for the work to be done, and ideally, owners that are not on the lang team. Simply dividing work though can lead to conflicts later on, so we also need frequent communication and updates to ensure that everyone is keeping abreast of the overall direction things are going, and to surface concerns early.
  • Efficient, open processes with tooling support: We are always looking for ways to improve how we operate to help us stay on top of what is going on in the Rust project and to reach conclusions more quickly. One thing we've noticed is that processes that are supported by bots or other tooling tend to work much better.

How you can help

Join the rust-lang Zulip, and either start a thread in the #t-lang/roadmap-2024 stream, or send a private message to Josh Triplett and nikomatsakis if you'd like to discuss privately first.

The plan (so far)

Current active initiatives in each category include:

  • See the status at a glance:
    • The initiative project board tracks all the currently active initiatives that we are focusing on. For each one, it shows their current stage along with their owners and lang-team liaisons.
    • During the backlog bonanza meetings, we are going through each older tracking issue and identifying what kinds of work is needed to move it forward (needs a summary, needs design work, etc).
    • We're taking the time to stabilize features that people are using, and remove incomplete features as well as features people are not using, with the eventual goal of treating everything open as "in-flight" rather than "unknown". We will also reduce the total number of in-flight features.
  • Clear owners and clear communication:
    • The initiative system assigns each task an owner, who drives the design, as well as a lang-team liaison, who helps ensure alignment with the team. More work is needed to get this system up and running smoothly.
    • We are launching a formality team that will take ownership of ensuring Rust's type soundness and diving into the details. This will help to grow the set of people with expertise in that area while also allowing the main lang team to consult as needed.
  • Efficient, open processes with tooling support:
    • We have designed a new consensus decision process that is designed to overcome some of the shortcomings we've seen with rfcbot; it needs to be implemented. This will help us make easily reversible decisions easier, enable more experimentation, make it smoother to raise and resolve concerns, and keep track of potential issues from proposal through to stabilization

Looking forward

Looking beyond the initiatives that are in progress, there's a lot of room for more improvement. Here are some other ideas we'd like to see. If you might be interested in giving that a try, come to #t-lang/roadmap-2024 to discuss, or send a private message to Josh Triplett and nikomatsakis.

  • See the status at a glance:
    • Find ways to integrate the older tracking issues with active initiatives; reduce the manual updates required to keep the project board in sync.
    • Improve the visualization of projects and blockers to something more compelling and easier to follow.
  • Clear owners and clear communication:
    • Beyond the type system, there are other areas where forming specialized teams could be useful.
  • Efficient, open processes with tooling support:
    • Generally improve rustbot to make meetings more efficient.
    • Improve and automate the process of going from initiative proposal to tracked initiative.

Conclusion

We hope that this post has given you a taste for what we plan to focus on over the next few years. If you'd like to help us reach these goals, please get involved! We've listed a number of active initiatives for each point, but we've also included a lot of ideas that are looking for an owner. Whether you prefer to code, design, document, or organize, there's work for you to do. And if the only thing you want to do with Rust 2024 is to use it, we welcome that too. Happy Rust hacking to y'all!