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).

Key links:

Initiatives

A lang team initiative is some active effort with a clear goal or deliverable. Typically initiatives are changes to the language, but they could also be documentation, specifications, or something internal to the lang team.

Active initiatives

Active initiatives are initiatives that have been assigned a lang-team Liaison and which are actively underway. A complete list can be found in this GitHub project board. Note that this list doesn't represent all unstable features; older features in particular were added without active initiatives.

Each initiative on the project board is linked to a tracking issue and has a status:

  • The Owner and Liaison are assigned to the issue.
  • If the initiative has a dedicated repository, the issue is created on that repository (some initiatives don't require their own repos; they are found on rust-lang/rust or rust-lang/lang-team).
  • The Stage of the initiative:
    • Experimental -- Drafting RFC; implementation work may begin on nightly as well
    • Development -- Approved RFC; implementation is in progress on nightly
    • Feature complete -- Implementation is complete on nightly and ready for widespread testing
    • Stabilized -- Implementation is complete and available on stable
      • To be stabilized, there must be a pending PR adding the feature to the Rust reference, but this PR may not yet have landed.
      • Other forms of integration, such as rustfmt, often take place after stabilization as well.

Proposed initiatives

You can see the currently proposed initiatives on Github. We review this list during triage meetings and decide whether to assign a liaison or close the proposal. If initiatives haven't received a liaison after 6 weeks of activity, we take that as a sign that there is no enthusiasm to pursue this and close the issue, but you are welcome to re-open the issue if you believe someone would be willing to liaison.

How does one propose a new initiative?

It's easy! You just open a short issue describing your idea. Read more in the process page!

Priorities

This page describes the current lang team priorities and explains their motivations. This page is typically updated as part of the yearly Rust roadmap process. They are derived from a combination of the Rust survey results, feedback from users, and other soruces. When new project proposals are created, they can cite priorities listed in this page.

Each priority also lists lang-team members who typically prefer to liaison for issues in this area. This can give you an idea of who you might reach out to if you wish to discuss a project proposal.

Last updated: 2020-07-28

  • Async I/O
    • What? Continued improvements with ergonomics and productivity related to Async I/O.
    • Why? Shows up heavily on the survey, this is an obvious area where a lot of Rust developers are working.
    • Who: nikomatsakis, withoutboats, cramertj
  • C Parity, interop, and embedded -- these often overlap in 'low level capabilities'
    • What? Extending Rust's low-level capabilities to do "things otherwise only possible in C or assembly", as well as enabling smooth, ergonomic FFI between other languages and Rust.
    • Why? Embedded is a large factor in the survey.
    • Why? Our ability to act like "native C" is a differentiating capability for Rust. We've seen a lot of traction integrating into big companies on this basis, as a C++ replacement. It's clear that doing this requires the ability to do piecewise adoption.
    • Who: joshtriplett
  • Const generics and constant evaluation
  • Trait and type system extensions
    • What? Specifically impl Trait, GATs, and specialization
    • Why? Long-standing areas that affect a lot of domains, including async
    • Who: nikomatsakis
  • Error handling
    • What? Combination of library related improvements that consolidate "best practices" into standard library, documentation to describe how it works, as well as possible language improvements to leverage those changes (try blocks, yeet/throw keyword, etc).
    • Why? Cross-cutting productivity concern, and a persistent problem that makes working with Rust code more difficult than it should be.
    • Why? Anecdotally, something that comes up for a lot of people (see e.g. nrc's #rust2020 blog post)
    • Who: withoutboats, joshtriplett
  • Borrow checker expressiveness and other lifetime issues
    • What? Think Polonius, RFC 2229, RFC 66, and other ideas like knowing which fields of self are used by particular methods.
    • Why? Learning curve remains a stubborn problem, and the best way to improve it is to make the compiler smarter.
    • Who: nikomatsakis, pnkfelix
  • Unsafe code capabilities and reference material
    • What? Document the rules for legal unsafe code and add features that either add required capabilities or make correct code easier and more ergonomic to write.
    • Why? Growing base of unsafe code, changes here are getting harder, this represents a kind of "reputation risk". We really want to be "better than C" here.
    • Who: nikomatsakis, pnkfelix
  • Targeted ergonomic wins and extensions
    • What? Small additions or improvements to make Rust easier to use.
    • Why? These will never rise to the "top of the list", but they often have outsized impact on people's enjoyment of Rust.
    • Who: scottmcm
  • Soundness holes to try and correct
    • What?
    • Why? Rust's appeal rests on safety. We have to make steady progress on these points. It's often hard to prioritize them compared to "whiz-bang" features. Also, long-running safety issues can cause fallout when fixed, weakening our stability guarantees.
    • Who: nikomatsakis

Stabilized initiatives

This table lists past initiatives which were stabilized.

Process

This page describes how lang team initiatives work. This is the process to use if you have an idea for a change you would like to make in the language.

Summary

In a nutshell, the process for a successful initiative is as follows:

  • Have an idea
    • Talk about it on internals, Zulip, etc to flesh it out a bit
    • Ideally, identify a potential owner
  • Open a proposal as an issue on the lang-team repository
    • A lang team member can decide to be your liaison and second your proposal.
    • Once that happens, we will create a Zulip stream, tracking issue, and (optionally) repository, etc.
  • If warranted, explore the design space and author the RFC
    • In this phase, the owner works with the liaison and other contributors to expore the design space and develop the RFC
    • Code can be landed in this phase, but the feature gate is marked as "experimental" and its associated page in the Unstable Book also incudes a disclaimer that the content has not been RFC'd.
    • Users of the feature gate will get a warning that the RFC is under development
    • Once the RFC is ready, it can be opened on the RFC repository and approved by the lang team
  • Finish development
    • At this point, development proceeds but the feature gate does not have to be marked as "experimental" (the unstable book can also be updated).
    • The goal here is both to implement the feature and to create an "explainer" that guides people on what it does (this can be located in the unstable book or elsewhere).
  • Feature complete
    • When the liaison feels that the code and explainer are ready, the initiative may be declared "feature complete".
    • This is primarily a 'signaling' mechanism to the broader community to encourage testing and feedback.
    • This is a good phase in which to write the Rust reference chapter and other supporting documentation.
    • Presuming feedback is positive, a stabilization report is prepared and (hopefully) approved.
  • Stabilized
    • Done! The Zulip stream can stick around as a place for further discussion, but the initiative is complete. Any Rust team and repository is typically archived.
    • The final step is to conduct a retrospective discussion between the owner and liaison about how the process went.

Goals

  • Empower individuals and give ownership:
    • Each initiative in this proposal is ultimately owned by a single person who drafts the proposals and recommendations.
    • The role of the lang team is to review the designs, provide feedback, and ultimately decide whether to accept the design.
    • The team can introduce constraints and requests that the owner should either satisfy or explain why they are not able to do so.
  • Clarify the role of each individual:
    • As described in the roles page, each individual and group involved in an initiative has a clear, defined role in the decision making process.
  • Minimize friction for "reversible" decisions and enable experimentation:
    • We avoid requiring "full checkoff" from team members for things that can be readily reversed.
    • We want to make it relatively easy to start hacking and experimenting with an idea. Under this proposal, all it takes is to find an owner, a liaison, and to have the team leads approve.
    • Other team members are encouraged to log concerns and constraints that ought to be addressed in the design, rather than blocking experimentation.
  • Ensure that decisions are truly reversible:
    • On the flip side, although we wish to make it easy for ideas to move forward, we recognize that this can create a lot of momentum that allows ideas to force their way through the process.
    • This is why code in the experimental phase issues a warning, for example.

Roles

There are several key roles for any initiative:

  • The owner assembles the "menu" of overall solutions to the problem and helps to narrow them down to the most viable.
    • Naturally, they are encouraged to make recommendations on what they consider to be the right choice. These recommendations should include an explanation of what analysis they did on the other options that led them to this recommendation.
  • The liaison represents the lang team:
    • They guide the owner in picking from the menu. Naturally they may also make suggestions, ask questions, or request that the owner expand or explore a fresh area.
    • They identify when to involve the full team via a design meeting.
    • They keep up to date on the initiative's progress and can summarize its status for other lang team members.
  • The lang team members make the ultimate decision on what changes to accept.
    • They don't typically do the design, though of course they may make suggestions or introduce constriants that the design ought to meet (the owner can argue that these constraints are undesirable).
  • The lang team leads make the ultimate decisions on prioritiziation (which initiatives to pursue).
    • They also help resolve other disputes that fail to come to agreement

The owner and liaison are each a single person, though that person can change over time. They cannot both be the same person.

Initiative owner

The owner of an initiative is the person who is ultimately responsible for its design and implementation. They are driving and managing its overall process from conception to completion, though they may work with others to manage pieces of tha process (for example, the implementation is often done by someone else).

The owner is not the decision maker, but they have a huge influence on the design. They assemble the "menu" of overall solutions to the problem and help to narrow the choices down to the ones that are most viable. They are also encouraged to make recommendations on what they consider to be the right choice. These recommendations should include an explanation of what analysis they did on the other options that led them to this recommendation.

Owners do not have to be members of the lang team, but they do have to be sufficiently experienced and dedicated to drive the initiative with minimal assistance. Owning an initiative is also a common stepping stone on the path to full lang team membership.

Pre-requisites

  • Sufficient experience to perform or mentor the above tasks independently
  • Demonstrated good judgement

Tasks

Note that in practice these may be delegated or done in concert with a group:

  • Exploring the design space and preparing the final design
  • Escalating tricky decisions by defining the 'menu' of choices and alternatives for the team to consider (with recommendations, where appropriate)
  • Documenting the design space and alternatives that were not chosen (and why)
  • Interacting with people on Zulip or elsewhere who are offering feedback and ideas, incorporating those ideas where appropriate into the final design
  • Developing and writing the code for the feature
  • Documenting the feature in the Rust reference or other sites as appropriate

Estimated time commitment

The time commitment will vary depending on the initiative. Some initiatives are effectively full-time endeavors (40 hours per week). Others can move along at a pokier pace. For an initiative to be considered active, however, it should have some form of update most months. After several months of inactivity, we will discuss with the owner whether the initiative should be moved to "inactive" state or whether we should seek another owner.

Initiative liaison

The liaison for an initiative is a lang team member who is responsible for tracking its progress and giving updates to the lang team. They should also help advise the owner on how to resolve issues and when to seek the feedback of the broader lang team or other stakeholders.

The liaison and the owner cannot be the same person.

Tasks

  • Mentoring the initiative owner and helping them to decide when facing difficult design decisions.
  • Preparing a summary for the monthly planning meeting documenting the decisions that were made and why.
  • Escalating important decisions to the team where appropriate for broader feedback.
  • Generally serving as a kind of "outside voice" where necessary.

Pre-requisites

  • Lang team member
  • Interest in the initiative and sufficient context to help guide (don't have to be an expert, but should be able to recognize what you don't know)

Estimated time commitment

15-30 minutes per week (regular sync meeting) and the occasional deep dive. If the liaison is spending much more time than this, then they may in fact be playing the role of owner, and that is a problem.

Stages of an initiative

Presuming that an initiative continues successfully, it will go through the following stages:

PhaseGoalPermitsSuccessful exit
ProposalFind a team liaisonDiscussionTeam member seconds, thereby agreeing to act as liaison
Experimental
(sometimes skipped)
Refine the design and work towards an RFCActive zulip stream; tracking issue / repository; code can land under "unstable" feature gateRFC is approved by team
DevelopmentFinalize design and implementationRemoving the "experimental" tag on a featureLiaison declares the proposal feature complete
Feature completeGathering feedbackAdvertisting the initiative as "feature complete"Stabilization proposal approved
StabilizedUse on stable branch(none)

The "experimental" stage can be skipped if the initiative is sufficiently simple that a RFC doesn't seem necessary; see "simple initiatives" below.

Initiatives can also become inactive for a variety of reasons.

Simple initiatives

The above process is the ideal, but there are alternative routes that occur in practice for simple initiatives. The most common is one that begins with a PR:

  • Implement the idea and open a PR.
  • Nominate the PR to the lang team's attention.
  • The lang team may opt to either approve the PR (skip directly to "feature complete"), assign an owner (skip directly to "development"), or to request a proposal.

Umbrella initiatives

Some initiatives are broader in scope and are tagged as "umbrella initiatives". These are far-reaching tasks like "async I/O". While they have goals, they don't have a clear stage. Instead, they have project boards (and potentially subinitiatives) of their own that track their state.

Diagram

graph TD
    HaveGoodIdea[Have a good idea]
    FileLangProposal[File proposal issue<br>on lang-team repository]
    RejectWithExplanation[Proposal closed,<br>with an explanation of why]
    Experimental[Expermental: Iterate on design and develop an RFC]
    Development["Develop implementation"]
    FeatureComplete["Feature complete"]
    Stabilized["Stabilization is approved"]

    HaveGoodIdea-- Idea seems ready -->FileLangProposal
    HaveGoodIdea-- iterate on internals/zulip/etc -->HaveGoodIdea

    FileLangProposal-- Concept declined by lang team -->RejectWithExplanation
    FileLangProposal-- Liason seconds, no RFC required-->Development
    FileLangProposal-- Liason seconds, RFC required -->Experimental

    Experimental-- RFC is approved -->Development
    Development -- Iterate and improve design --> Development;
    Development -- Ready for people to experiment --> FeatureComplete;
    FeatureComplete -- Decide to make changes --> Development;
    FeatureComplete -- Prepare stabilization report --> Stabilized;

Stage 1: Proposal

Summary

The proposal stage is where specific ideas start. To make a new proposal, open an "Initiative Proposal" issue on the lang-team repository. It should include:

  • Motivation and general idea
  • Any relevant background links
  • Enough detail to understand the problem and some sense of how it might be solved
  • Someone interested in being the owner for the initiative, if any.

Proposals are meant to be raised early. The idea should have undergone some amount of iteration, perhaps on internals or elsewhere, but it doesn't have to be -- and ideally is not -- a fully formed, concrete proposal. It can be a sketch of "here is a problem and here are a few general ideas for how to address it".

Once a proposal is opened, it can be discussed asynchronously by lang team members. If some member likes the idea, they first check with the team leads. If the leads agree, the member can second it, which means the member agrees to serve as the lang team liaison.

  • Proposal does not have to include:
    • Specific details or a known plan, uncertainty is expected
  • Lang team members will review, consider, and discuss

While a proposal is open, it can undergo refinements simply by editing the issue text. Discussions typically take place on Zulip, but it is expected that regular summaries will be posted.

Timeframe

Proposals represent pending decisions and are not meant to stay open. The decision about whether to accept a proposal or not is typically made within 1-2 weeks and is meant to decided within one month. In some cases, we may close a proposal but continue discussing and decide to open a fresh proposal shortly thereafter.

Exit: Seconding a proposal

Any team member can second a proposal: this means that they volunteer to serve as initiative liaison. Before seconding a proposal:

  • The proposed owner should ensure that all relevant conversation from the Zulip threads, internals forum, and other forums is summarized in the issue.
    • If they are not willing/able to do this, they are likely not a good choice to act as owner.
  • The liaison should check with the team lead(s).
    • This gives the leads a chance to discuss whether the initiative seems like a good fit and whether the proposed initiative owner/liaison is a good choice (and, if not, why not).
    • Team lead(s) should check with the moderation team to see if the proposed owner (or prominent likely members) has any prior history that may not be known to them. The leads do this to keep this info on a narrow basis.
    • It's better to have those kind of sensitive discussions before things have been said publicly.

After this is done, members may second the proposal by writing @rustbot second in the issue thread along with a short comment identifying the owner. Seconding a proposal will cause it enter an FCP period. Team members may opt to suspend the FCP period by issuing @rustbot pause, this will cause the FCP to suspend. This is typically done to raise a concern which can then be discussed. The FCP can be resumed by any member saying @rustbot resume (to re-enter FCP) or @rustbot cancel (to cancel FCP).

Once a proposal is seconded, the next step depends on its complexity:

  • Most initiatives proceed to Stage 2 (Experimental), which is focused on authoring an RFC.
  • However, simple initiatives can skip directly to Stage 3 (Development).
    • This indicates that the design is well understood and there aren't any complex tradeoffs to explore and document.
    • A common example of this is for new lints.

Types of objections that make sense at this period:

  • Technical concerns:
    • These are typically managed by adding the scenario or concern to a list of constraints to be taken under consideration and addressed.
    • It is absolutely not necessary to have answers to all the technical problems before seconding a proposal!
  • Overload concerns:
    • A single owner or liaison should not be involved in too many things.
  • Prioritization concerns:
    • This idea might seem premature or like a poor choice of resources.
  • People concerns:
    • Concerns about the people involved are best raised by taking directly to the leads first.

Exit: Proposal is not accepted

In some cases, proposals will not be accepted. This could happen because there is nobody who wants to second it at this time. It could also happen because the lang team leads decide that the proposal doesn't it the current priorities of the team, or because the lang team leads feel that the task is not a good fit for the proposed owner or liaison (for example, the task may require specialized skills or more time than they have available). In these cases, the proposal will be closed.

We typically do not close proposals just because lang team members have technical objections: instead, those objections can be logged and resolved through the experimentation period. However, if it is clear that the proposal has no pathway to being accepted, we would try to take that into account.

Stage 2: Experimental (optional)

If an initiative is sufficiently complex as to warrant an RFC, then, after being seconded, the initiative enters the "experimental" state. The goal at this stage is to iterate on exploring and documenting the design space and preparing an RFC, or even multiple RFCs. These initiatives have their own Zulip stream and can land code in the compiler.

Being in the experimental stage does not represent a commitment to land the code. There may well be team members with very live concerns about the feature and it may well get removed if those concerns cannot be resolved.

Initiatives in the experimental stage can have the following resources:

  • Their own dedicated Zulip stream (#project-xxx)
  • A tracking issue
  • A repository if desired
  • They can land code in the compiler under an "experimental" feature gate (i.e., one that warns when you use it)
    • Ideally we would warn that this represents an early stage, "experimental" proposal

Skipping this stage

Some initiatives are rather simple. In that case, we can skip this "experimental" stage and go straight towards development without authoring an RFC. This often applies to small tweaks in the language, or to things like adding a new lint.

Advertising that a feature is experimental

Feature development that occurs in the experimental stage is very tentative. We must take care to ensure that users are aware of this. All feature gates related to "experimental" initiatives need to be flagged as experimental and issue a suitable warning to users. Furthermore, the "unstable book" page for the features should indicate that they are currently experimental and do not yet have an accepted RFC.

During this stage: updates to the team

During this stage, the owner and the liaison should meet on a regular basis. The owner should update the liaison about major design directions and seek their guidance on complex issues (particularly if the owner is not a member of the team). The liaison is responsible for documenting these updates and preparing a monthly update to the team as a whole. They are also responsible for deciding when an issue should be escalated to a lang team design meeting. Sometimes it makes sense to have a design meeting even if there isn't a decision to be made, just to update the team about the overall progress.

Exit: RFC approval

To exit the Experimental stage, you typically need to prepare an RFC on the rust-lang/rfcs repository. This RFC needs to be approved by the lang team. The RFC can include "unresolved questions" to be resolved during the "development" phase.

Exit: Go inactive

It often happens that initiatives "stall out". This could be because some of the problems seem insurmountable, because the people involved wind up not having enough time to continue, or because other things take priority. At any point, the owner can decide to step back, at which point the initiative becomes inactive. If an initiative has not made progress in several months, the team may also opt to move it to inactive status.

Stage 3: Development

After an RFC is approved, the initiative enters "development" stage. The only difference from the "experimental" stage is that the feature gate can now be marked as "non-experimental" and hence used without any sort of warning.

During this stage, the focus is on implementing the complete feature and on resolving any unresolved features.

Exit: Group declares feature "feature complete".

At some point, the group can declare a feature to be "feature complete". This requires the following materials to be available:

  • Accessible documentation in the "unstable Rust" user's guide, if appropriate.
  • All unresolved questions from the RFC have documented answers.
  • Tests are written covering all major points in the RFC.
    • Ideally, those tests should be documented, as well, though we don't have a real convention here.

Exit: Go inactive

It often happens that initiatives "stall out". This could be because some of the problems seem insurmountable, because the people involved wind up not having enough time to continue, or because other things take priority. At any point, the owner can decide to step back, at which point the initiative becomes inactive. If an initiative has not made progress in several months, the team may also opt to move it to inactive status.

Stage 4: Feature complete

"Feature complete" initiatives are awaiting community experimentation and stabilization. Typically this is done by writing a blog post (on Inside Rust, perhaps) encouraging experimentation and feedback. That feedback should be gathered and summarize in the monthly reports. It is particularly useful to have lists of users.

The owner should continue to meet regularly with the liaison at this time, though the meeting frequency is often just once a month and quite short.

Entry criteria

Entering the "feature complete" stage typically requires that there is documentation available to users about how the feature works overall (but not necessarily detailed reference material):

  • Liaison agrees that the feature is feature complete.
  • An explainer is prepared that explains to end-users how the feature works.
  • A blog post, linking to the explainer, that announces progress and requests testing and feedback.

In addition, it's a good place to explain how any "unresolved questions" from the RFC would up being resolved.

Preparing reference documentation

In addition to evaluating the feature, this is a good period to prepare "reference" documentation that explains the changes in depth. This is typically included in the Rust reference but may appear in other documentation as well, such as the Necronomicon. These changes will be reviewed as part of the stabilization report and will land after the feature is stabilized.

Exit: Stabilization report prepared and approved

To exit the stage, the owner prepares a stabilization report. Stabilization reports follow the templat and generally give details about:

  • Final design of the feature
    • In particular, how were unresolved questions or other details from the RFC resolved?
  • What is tested and where
  • Link to the Rust reference materials or other documentation

Stage 5: Stabilized

This is the final stage. The development is done and support transitions to the regular team members. The initiative is now considered inactive. However, the stream often sticks around as a convenient place to ping the people who were involved in implementing the feature for follow-up questions and for fixing bugs or other maintenance. The expectation is that if you helped to develop a feature, you will stay involved for some period of time after it hits stable to help in resolving problems that arise.

After stabilization, the owner and liaison should meet to do a final retrospective on how everything went and what could have been improved. This should be done within a few weeks of stabilization, while the memories are fresh.

Inactive groups

As an alternative to stabilization, initiatives may become inactive. This could be because we decided the idea doesn't work, but it could also be that people just no longer had time to work on it and so the idea was paused until somebody else shows up with time and energy. When marking an initiative as inactive, we try to do a final retrospective that judges how well the experience went. We also try to prepare a summary of the final state in case someone wants to start working on the design again (or perhaps a small piece of it; a common failing for initiatives is trying to tackle too big of a problem). These technical summaries can be placed in the design notes section of this website.

Checklists

Here are some checklists of the exact procedural steps to take.

Open a proposal

Second a proposal

  • If there has been significant conversation, liaison should ask owner to summarize that on the issue.
  • Liaison should write @rustbot second which will trigger the start of the 10-day Final Comment Period (FCP).
  • Any lang team member who has concerns should leave them in Zulip and on the issue during the final comment period.
    • Owner and liaison are responsible to describe how they expect to resolve the concern. This can be something simple, like "we will do a write-up and a design meeting on this point".

Approve a proposal

Once a proposal has been seconded and the FCP has expired:

  • If the initiative wants its own repository, then open an issue on the infra-team repository and request that the infra team clone the initiative-template repository with whatever name you need (your-initiative-here).
  • Create a tracking issue to track the initiative:
    • If should be on rust-lang/your-initiative-here if you have a repository
    • Otherwise it should be rust-lang/rust or rust-lang/lang-team
    • Tag it with C-tracking-issue and T-lang
  • Add the tracking issue to the project board:
    • Set the status appropriately (typically "experimental").
  • If necessary, add a feature-gate to the compiler and tag it as "experimental".
  • Close the proposal issue with a link to the tracking issue.

Close a proposal

  • After 4 weeks without a second, rustbot will post (not yet implemented) and add "disposition-close" and "final-comment-period":
    • "Hi there @rust-lang/lang and @author! It has been 4 weeks and this initiative has not been seconded. Initiatives that are not seconded after 6 weeks are automatically suggested for closure. Members of @rust-lang/lang, please take a look and see if you want to second this! -- your friendly neighborhood rustbot"
  • After 6 weeks without a second, rustbot will post the following and add "timer-elapsed"
    • "Hi there @rust-lang/lang and @author! It has been 6 weeks and this initiative has not been seconded. It seems that there isn't bandwidth to take up this proposal right now. I am tagging this issue to be closed at the next triage meeting. Please feel free to reopen it in the future when more bandwidth is available, especially if you are able to find a lang team member who says they will second it. -- your friendly neighborhood rustbot"
  • Human can then close the issue from the triage meeting with comment sort of like:
    • "Thanks for the proposal @author. Unfortunately there wasn't a second at this time and so I am going to close the issue."

Exit the experimental stage and enter development stage

  • Write an RFC
  • Get the RFC approved
  • xxx fill this in

"Frequently asked questions" about initiatives

Does the initiative owner make decisions?

The initiative owner drafts the proposed design and takes feedback from the liaison and team about what direction to take. This feedback can take the form of "you need to do X", but typically it is more about "you need to address this scenario". Or, put another way, if the initiative owner doesn't like the proposed direction, it's up to them to find an alternative that they do like which meets those same constraints, or to argue why the constraints are not necessary.

Note that serving as initiative owner is a high level of responsibility and may not be a good "starting place" for involvement within the Rust project. In practice, initiative owners should be experienced enough that they could mentor others to do the implementation work. If you don't know the language or system well enough to do that, then you probably are not ready to be an owner -- but you may be ready to be mentored by the owner!

Is the word of a lang team member law?

Of course not. Well, ok, sometimes. For the most part, initiative owners are encouraged to treat lang team members like any other member of the community -- this implies a lot of respect for their opinions, since they are experienced, knowledgeable people, but the initiative owner still ultimately owns the design and should use their own judgement about what things to recommend. However, lang team members do have the option of adding constraints that must be met, and they can override the initiative owner if necessary. That is typically done by raising the concern with the rest of the team/leads in a more formal way.

What happens if an owner stops working on things?

Initiative owners are often volunteers and may have changes in priorities or find they don't have as much time as they thought they did. In that case, they can simply step back. The liaison can then either find a new initiative owner, or perhaps assume initiative owner duties themselves but find a new liaison. If they are not able to do that, the initiative will be closed as "paused".

What if we decide a initiative is a bad idea?

Sometimes, in the course of trying to design a initiative, we decide it was the wrong direction. That's ok! At any point the liaison can decide that the initiative isn't working out and close it. However, in doing so, they should document WHY they feel it did not work out -- and identify potential conditions where the idea may make sense later on. This documentation will typically take the form of a design note in the lang-team repository.

If there are concerns about this, those concerns can be raised with the lang team leads.

Closed initiatives will be removed from the project board and the code for them will be removed from the compiler.

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.

Lang team "how to" guide

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

Nominate an issue

  • 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)
  • Tag the issue with the I-nominated and T-lang labels by issuing the following rustbot command:
    • @rustbot label +I-nominate +T-lang
  • Add a comment explaining
    • What question you would like answered by the lang team
    • Relevant background info or links to relevant info that the lang team can use to get "oriented" in the discussion

Propose an initiative

See the initiative process for more details.

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.

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, 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.

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!