Welcome
Welcome to the repository for the Rust Language Design Team. This page stores our administrative information, meeting minutes, as well as some amount of design constraints. It’s always a work-in-progress (insert omnipresent mid 90s logo for under construction here).
What is the lang team
The lang team generally governs the “surface area” of the language, meaning both what code compiles and what happens when it executes. For new language features, we also assume a general “project management” role, in that we track the feature as it progresses from an idea, to implementation, and finally to stabilization.
Note that implementation itself is governed by the compiler team. To be very concrete, the lang team controls the spec, and the compiler team approves the implemenation and ensures that it meets the spec. Naturally, though, development is a collaborative process, and it often happens that the spec is altered in response to concerns that arise during implementation.
We also work closely with the types team, which owns the details of the type system design in much the same way as the compiler team owns the details of the implementation.
Finally, there are often “grey areas” between the language and library teams, such as the addition of a new standard library trait that reflects a core system capability (e.g., Future). In those cases, the lang team is generally reponsible for deciding if we want the core capability, the libs team owns the API details.
Key links
- Active initiatives project board
- Shows you the things that are currently under development (or exploration) within Rust.
- You can read more about initiatives here.
- Meeting calendar, triage meeting minutes, design meeting minutes
- You can read more about our meetings here..
Lang team “how to” guide
This section includes instructions on how to do various interactions with the lang team.
Nominate an issue
You can raise issues to lang team attention by tagging them with I-lang-nominated. We scan through nominated issues during our triage meetings. For each issue, we try to answer questions and reach decisions on the question being discussed in the issue.
How to nominate
Add a self-sufficient comment to the issue that explains why you are nominated the issue and includes the text @rustbot label +I-lang-nominated. For example:
@rustbot label +I-lang-nominated
I am nominating this for lang-team attention. We have been discussing the pros/cons of updating the type-checking rules for foo bar. The options on the table are as follows:
* Allow type mismatches: This is good because blah.
* Disallow type mismatches: This is good because blah.
Where should we go from here?
The ideal comment will identify precisely what question you would like answered. Please try to make the comment easy for us to parse and understand without requiring a lot of context. We encourage links to internals or Zulip so we can dive into the details, but it really helps us give useful answers if you can summarize the key details up front.
If you’re not a member of the Rust team on GitHub you’ll get an error from rustbot when trying to add the label - please make the comment (following the guidelines above) anyway, and ping the lang team on our (chat platform)[../chat_platform.md) asking for the label to be added.
If your comment requires more than 5-10 minutes of reading and discussion to understand and effectively respond to, consider filing a meeting proposal instead. This will give you ~60 minutes to present your question and the lang team more time to analyze it. We may punt the question back to you with an ask to do so if the question isn’t answerable in our triage meeting time.
How quickly will the lang team answer?
We try to be prompt, but sometimes we are not. Othertimes, we discuss the issue, but fail to leave the follow-up comment, because we’re only human. Please feel free to raise the topic on Zulip or reach out to a lang team member.
Repositories where we look for nominations
Nomination is currently supported on the following repositories:
- rust-lang/rfcs
- rust-lang/rust
- rust-lang/reference
- rust-lang/lang-team
(This set is defined by the nominated list in the triagebot source code)
Proposing a topic for a design meeting
You can propose a topic for a design meeting by opening an Design meeting proposal issue on the lang-team repository. We schedule meetings in our monthly planning meeting.
Every design meeting begins by reading a document and leaving comments. That document must be prepared 24 hours in advance of the design meeting and posted on the issue. If you propose an issue, you should be willing to prepare that document, or else indicate who will do the preparation.
Typically, design meetings are associated with active initiatives: you may wish to propose an initiative instead.
Proposing a change to the language
Do you have an idea for a new language feature? The following page describes the lang team policy around changes to the language.
TL;DR
The highlights are these:
- An RFC is required for most changes.
- Small changes that fit in a PR, have narrow impact, and are uncontroversial can skip the RFC process (read more).
- If you have a lang-team liaison and an experienced implementor, you can start experimenting by just adding a feature gate and a tracking issue, but you’ll still need an RFC later.
Complete flowchart
The following lays out the complete flow chart for language features. Note that you can click on most of the nodes to read about that step in more detail.
flowchart TD
subgraph LangTeamChangeProcess [Lang Team Change Process]
TweakToExistingFeature["How large is your proposed change?"]
LangTeamLiaison["Are you an experienced contributor\nwith a lang team liaison?"]
NeedToExperiment["Do you need to experiment\nbefore you can write RFC?"]
subgraph Stages
ExperimentalFeatureGateProposed["Create a tracking issue and\nopen a rust-lang/rust PR proposing\nan experimental feature gate"]
ExperimentalFeatureGateAccepted["Experiment is approved. Write code!"]
RFCOpen["Open a rust-lang/rfcs PR\nwith the completed RFC"]
RFCAccepted["RFC is merged.\nIf needed, create a tracking issue,\n and finalize the implementation"]
FeatureComplete["Implementation is complete.\nGain experience with the new feature."]
StabilizationProposed["Author a stabilization report and\nopen a PR stabilizing the feature"]
Documented["Author a rust-lang/reference PR\nextending the reference\nto describe the new feature."]
StabilizationAccepted["Stabilization is approved!\nFeature rides the trains into stable."]
ChangeProposed["Open a rust-lang/rust PR and\nnominate for lang team."]
ChangeAccepted["PR is merged."]
TypesTeamApproval["Types team approves the\ninteraction with type system."]
StyleTeamNotified["Style team notified about the new feature."]
ExperimentalFeatureGateProposed --Seconded--> ExperimentalFeatureGateAccepted
ExperimentalFeatureGateAccepted --> RFCOpen
RFCOpen --FCP--> RFCAccepted
RFCAccepted --> FeatureComplete
FeatureComplete --> StabilizationProposed
%% StabilizationProposed --> StabilizationAccepted
StabilizationProposed --do this--> Documented
Documented --> StabilizationAccepted
StabilizationProposed --and this--> TypesTeamApproval
TypesTeamApproval --> StabilizationAccepted
StabilizationProposed --and this--> StyleTeamNotified
StyleTeamNotified --> StabilizationAccepted
ChangeProposed --FCP proposed and accepted--> ChangeAccepted
ChangeProposed -- If team feels change\nmerits a second RFC --> RFCOpen
end
TweakToExistingFeature -- Tweak to an existing aspect of Rust --> ChangeProposed
TweakToExistingFeature -- New feature or a complex change --> LangTeamLiaison
LangTeamLiaison -- Yes --> NeedToExperiment
LangTeamLiaison -- No --> RFCOpen
NeedToExperiment -- Yes --> ExperimentalFeatureGateProposed
NeedToExperiment -- No --> RFCOpen
end
%% Drawn from https://coolors.co/25283d-8f3985-a675a1-cea2ac-efd9ce
classDef pink fill:#EFD9CE
classDef tuscany fill:#CEA2AC
class LangTeamChangeProcess pink
class Stages tuscany
click RFCOpen href "https://github.com/rust-lang/rfcs/#when-you-need-to-follow-this-process" "Read about RFCs"
click RFCAccepted href "https://forge.rust-lang.org/lang/rfc-merge-procedure.html" "RFC merge procedure"
click ExperimentalFeatureGateProposed href "./experiment.html" "Read about experimental feature gates"
click ExperimentalFeatureGateAccepted href "./experiment.html" "Read about experimental feature gates"
click StabilizationProposed href "./stabilize.html" "Read about stabilization procedure"
click StabilizationAccepted href "./stabilize.html" "Read about stabilization procedure"
click Documented href "./stabilize.html" "Read about stabilization procedure"
click TypesTeamApproval href "./stabilize.html" "Read about stabilization procedure"
click StyleTeamNotified href "./stabilize.html" "Read about stabilization procedure"
click TweakToExistingFeature href "./propose.html#what-constitutes-a-small-addition-or-tweak-to-an-existing-feature"
click LangTeamLiaison "./experiment.html" "Read about experimental feature gates"
click NeedToExperiment "./experiment.html" "Read about experimental feature gates"
Frequently asked questions
What’s the difference between “champion decisions” and “FCP decisions”?
These refer to our decision process:
-
A champion decision means a lang team member or advisor is championing the idea. It does not represent team consensus—only the champion’s point of view. The champion nominates the issue for a triage meeting; if no one requests FCP escalation, the champion can proceed. We use champion decisions for experiments and other cases where we want to move quickly.
-
An FCP decision represents a significant commitment from the team. It requires sign-off from team members via rfcbot and follows a formal process for resolving concerns. We use FCP decisions for stabilization, RFC approval, breaking changes, and other cases where we are making a promise to our users.
Are RFCs required for every language change?
No! For small additions or tweaks to existing features, you can simply implement the change, open a PR, and nominate it for lang-team attention. If the change turns out to be complex or controversial, though, we may close the PR and request an RFC instead.
What constitutes a “small addition or tweak to an existing feature”?
We do not require an RFC for everything. Small changes that fit in a single PR, have narrow impact, and are uncontroversial can skip the RFC process. Simply make the change, open the PR, and nominate it for lang-team feedback. But be aware that we may still ask you to write an RFC! The rule of thumb is that we use RFCs for ideas that impact a lot of users or are potentially controversial. They’re a great way to get broad feedback from the community about an idea.
If you choose to open a PR without an RFC, please document the motivation and details of your change! Very often people will open a PR that changes some code in the compiler without clearly explaining what they are trying to achieve. Also, please make the explanation “self sufficient” – avoid linking to internals threads or other places where we have to read a bunch of context to understand what is going on (it’s encouraged, however, to provide a summary and link to threads for more details).
Some examples where RFCs are typically NOT required…
- Narrow changes like adding support for a new ABI (this may even just be a compiler concern).
- Soundness fixes to existing features, unless they have large impact or change the way people have to write Rust, in which case we may opt for an RFC.
- Small changes to complex rules, such as name resolution, intended to address a particular problem and which don’t impact most users; often in cases like these RFCs do not provide valuable feedback, and so we’ll instead focus on getting the relevant experts to weigh in. This category in particular still needs to have a detailed write-up.
- Extending a lint to cover more instances of the same general pattern, or to be more precise.
- Proposing a new lint that is narrow in scope.
What is the policy for adding a new lint?
We have a streamlined process for most lint proposals. See the How do I add a new lint? for details.
Do experiments always require an RFC?
In the diagram above, we show experiments as always leading to an RFC. This is typically the case because experiments tend to be for larger, more ambitious features. However, if the experiment turns out to be a relatively small change to the language – i.e., some change that would not require an RFC anyway – then you can skip the RFC and move straight to stabilization.
What about adding special traits and things to the standard library?
We consider intrinsics and “lang item” (things that need special treatment from the language) to be under the lang team purview. Specifically, the lang team governs the semantics and capabilities exposed by this new feature. The details of the API are governed by the libs-api team.
Whenever possible, though, we prefer to issue a quick approval for the “general feature” being discussed, and leave it to the libs-api team to decide where to apply it. For example, we approved the ability to add inherent methods to standard library types like u32 long ago, but the libs-api team governs what APIs are available.
My RFC has been waiting for a comment from the lang-team for a long time!
First off, I’m sorry, that sucks. We are aware that we need to do a better job keeping up with RFCs :/ (as long as it’s tagged with T-lang, we’ll know it exists). That said, what you can do is to nominate the RFC and we will discuss it during the meeting.
How do I propose a new lint, or extend an existing one?
For small lints, you can follow the policy for smaller changes: implement the lint, open a PR, and then nominate it for lang-team attention.
We may request an RFC if:
- the lint is on (warn or higher level) by default, and is expected to affect a lot of users
- the lint is controversial
- the lint sets a (new) direction for Rust – for example, changing an existing pattern to a different one, even if the pattern isn’t widely used
- e.g., deprecating a syntax to make room for a possible new language feature
If in doubt, you can always raise the idea on Zulip first.
Experimental feature gates
We use “experimental feature gates” to allow experienced Rust contributors to start implementing and exploring features even before an RFC has been written. This is particularly useful for larger features where we know we definitely want to solve the problem, but there are a lot of unknowns to work out before we can really create a coherent RFC – think of things like adding async functions or the like.
Process
If you are an experienced Rust contributor who would like to start an experiment in-tree, the process is as follows:
- Write-up a description of the problem you are trying to solve and the general shape of the solution you want to work on. Discuss it on Zulip or elsewhere to find a lang-team champion:
- The champion is the connection to the lang-team. They can check in with you from time to time to see how the work is going and relay those updates to the lang-team (of course, you’re always welcome to join meetings yourself too!). They can also help to discuss problems that arise.
- Lang-team members and advisors are eligible to be champions.
- Once you’ve found a champion, open a PR adding a new feature gate to the compiler and create an associated tracking issue.
- The PR and tracking issue should include a write-up documenting the motivation and outline of what they are trying to achieve.
- The lang-team champion will nominate the PR for discussion at a triage meeting. This is a champion decision—if no one requests FCP escalation, the experiment can proceed.
- Approving a new feature gate does not imply support for the feature. It implies only that the lang team thinks it is worth doing the experiment to see what results.
- Note to lang team members: If you have concerns about the feasibility or wisdom of the feature, the right course of action is usually to allow experimentation to continue, but ensure that your concerns are noted on the tracking issue. This allows the experimenters to try and gather data and address your concern.
- When you feel the design is ready, you write an RFC as normal with your proposal. The goal of the experimentation period is simply to gain experience and information so that a better RFC can be authored.
Frequently asked questions
What is the role of the lang team champion?
The lang team champion is the connection to the lang team. They should be available to discuss progress and generally track what’s going on, and they can also raise questions to the broader lang team during triage meetings and the like (of course, the meetings are open, so you’re also welcome to join if you are able).
I’ve got an idea, how do I find a lang-team champion?
We don’t really have a process for that, but circulating the idea on Zulip is a good idea, or perhaps reach out to lang-team members that you know.
Why is experimentation limited to experienced contributors?
We’ve found that it works best when the person driving the experiment is able to move independently and without mentoring. Most folks on the lang team have limited bandwidth, so when they agree to serve as champion, they are committing to meet regularly, give feedback on your progress, and to circulate ideas within the lang team, but they are not necesarily going to have time to help find solutions to problems beyond that ((many lang team members aren’t that familiar with the compiler details anyway).
What if I’ve got an implementation on a branch already?
What we’re really looking for in experimentors is commitment and the ability to see the work through. If you’re able to implement the idea in a branch, that’s good evidence. See if you can find a lang-team champion.
Can a lang-team member propose an experiment, too?
Lang-team members can be the one to propose an experiment, and can serve as their own champion, but they should find someone else to “second” the FCP on the feature gate PR.
What if I’m not an experienced contributor, but I have a mentor who is?
That’s fine, you can still open the PR, but your mentor should be the one to nominate it for lang-team consideration.
What if I’m an experienced contributor, and I want to mentor someone?
See the previous question.
As the experimentor, what do I do when I feel like I am ready to write the RFC?
Glad to hear the experiment was a success! Check in with the champion to figure out if they feel like it’s time to author the RFC. In particular, if people raised concerns about the design in the beginning, make sure that you have a good answer for them. Even better, show them the answer, and see if they are convinced!
As the experimentor, what if I feel like we don’t want the feature after all?
This is also a very useful finding! In this case, it’s best to write up a comment (potentially short) on the tracking issue reflecting the findings from the experimentation phase, and suggest to your lang team champion that the tracking issue may want to be fcp’d to close so we can remove the feature from the compiler.
It’s generally a good idea to do this for features that aren’t being actively driven to conclusion and are in the experimental phase, as they can easily accumulate otherwise.
As the experimentor, what if I run out of time to drive this proposal?
First off, it’s always ok to take a break. If you are going to step away for a long time, you should at minimum leave a comment with your current thinking on things – if nothing else, it’ll help you remember what was going on when you come back.
If you feel like you have to step away indefinitely, then discuss with the champion. They may be able to find someone else, or it may make sense to simply write-up your findings and remove the feature from the compiler.
As a lang team member, what do I do if I feel like my concerns are not being addressed?
Bring up your concerns with the lang team champion and try to work towards finding experiments or new approaches that can resolve them. If this feature is ever going to make it to RFC, they are going to need your agreement, after all!
What if I have a competing proposal? Can I land my own experimental feature?
Obviously the best is if you can combine your idea with the existing experiment, particularly if your idea is a minor variation. But it is also ok to have multiple competing experiments, if the idea is going in a different direction. Just follow the same process (find a lang team champion, etc). Be sure to note in the PR etc that you know this overlaps with the existing experiment and are looking to explore a different part of the design space. Also, it will be really helpful if you and the other experimenters can jointly maintain a kind of FAQ comparing and contrasting the two ideas.
Stabilize a feature
The final step in the language-change process is to stabilize a feature. Stabilization works as follows:
- Author a stabilization report:
- Briefly recap the feature’s design from the RFC – you don’t have to go into detail, we can re-read the RFC.
- Give detailed descriptions of how the feature’s design has changed since the RFC was approved!
- Summarize any major decisions that were made during the implementation process.
- Verify that the feature is fully implemented. Look for tests covering all the major pieces of the RFC and include them in the stabilization report.
- Provide answers to any “unresolved questions” listed in the RFC.
- Describe the implementation history of the feature (optional).
- Prior to stabilizing, we need to coordinate with other teams:
- An open PR editing the reference to describe the change is required. (You don’t personally have to author it, but there needs to be an open PR, and ideally one that has been edited and is approved modulo actual stabilization by one of the repo maintainers.)
- If the feature affects the type system, you should nominate for @rust-lang/types to check for their approval.
- If the feature adds new syntax, you should should nominate for the style team to get their approval.
- The lang team will read the report and eventually move to FCP. Per our decision process, this is an FCP decision, as stabilization is a significant commitment to our users.
See also the rustc-dev-guide’s stabilization guide.
Decision making
This page documents our decision making process.
Our goal
We want the ability to make designs that feel fresh, bold, and innovative. We do not want Rust to feel like it has been “designed by committee”, with all the interesting bits smoothed down.
We also want designs that meet Rust users’ needs and live up to Rust’s ethos of reliable, performant, accessible code.
These two goals can be in tension. The former pushes us to empower individuals. The latter pushes us to validate designs broadly. We use this decision making process to guide us in balancing those tensions.
Design axioms
Our decision making axioms are rules that we follow to help us achieve our goal. We try to satisfy them all, but if they come into tension, we prefer items that appear first in the list.
- No new rationale. We make decisions only after the rationale has been presented publicly and all relevant stakeholders have had a chance to present counterarguments.
- Not afraid to do the right thing. At the end of the day, we have to do what we feel is right. Sometimes this means breaking with tradition and precedent set by other languages. Sometimes it means taking a socially uncomfortable stance (but always with respect).
- Find common ground. When there is disagreement, look for solutions that address everyone’s concerns. Break up designs into smaller pieces if needed. But be sure that each piece solves an end-to-end problem on its own.
- Trust each other. Lang team members are expected to have demonstrated sharp instincts, humility, and the ability to hear and understand others. Sometimes you have to put your doubts aside and trust the others on the team.
- Treasure dissent. When someone raises a concern, we take it as an opportunity to improve the design, not an obstacle to be overcome. We invite people to elaborate and make sure we understand what’s motivating them before we decide how to respond.
Two kinds of decisions
We divide decisions into two categories:
-
Champion decisions are the preferred default. They are used for starting experiments and other decisions where we want to move quickly. A single lang-team member or advisor can champion a decision; others can raise concerns, but cannot block. A champion decision does not represent team consensus—it represents only the champion’s point of view. Any team member can request FCP escalation at any point—during the initial triage meeting or later—which converts it to an FCP decision. Once a champion decision has been escalated and FCP’d, it becomes durable in the same way as any other FCP decision.
-
FCP decisions represent a significant commitment from the team. They are used for stabilization, RFC approval, and other cases where we are making a promise to our users or taking a position we don’t want to reverse lightly. FCP decisions require broader team sign-off and follow a formal process for resolving concerns.
When to use which: Prefer champion decisions when possible—they have lower overhead and enable faster iteration. Use FCP decisions when the decision is significant enough that reversing it should require another FCP. The expectation is that an FCP’d decision cannot be reversed without some change in circumstances: new experience, new information, or a reasoned change of mind.
Team size
The lang team’s ideal size is 4-8 members. This keeps the team small enough for high-bandwidth communication and trust, while large enough for diverse perspectives.
Champion decisions
Champion decisions do not represent team consensus. Rather, they indicate that somebody on the team is willing to champion an idea. We use them to begin experiments and for other decisions where we want to move quickly and iterate.
When to use champion decisions
- Starting or stopping a lang-team experiment
- Closing an RFC or issue that is clearly not going to be accepted
- Significantly changing an existing experiment’s scope or goals (either cutting or expanding scope)
- Opting to focus on a particular design direction and halt investigations of others
- Adding a new constraint to the design that you’ve learned as part of exploring the design space
- Other lightweight decisions that you’d like the team to be aware of, but that don’t make a durable commitment
Process
For the champion (lang team member or champion)
- Write up your proposal on a GitHub issue or PR, explaining the decision and context.
- Nominate it for discussion at a lang-team triage meeting.
- At the triage meeting, the team will discuss and raise any concerns.
- Consider the feedback. If team members expressed strong concerns, you should likely address those before proceeding.
Handling concerns
When people raise concerns:
- Treasure dissent. Engage with them and make sure you understand the concern.
- Note concerns on the tracking issue as “unresolved questions” or things to explore—they shouldn’t be forgotten.
- Concerns don’t block you from proceeding, but they may give you pause, particularly if they are shared by multiple team members.
For other team members
- Your approval is not required for champion decisions.
- If you disagree with the decision or think it’s a bad idea, say so (constructively)!
- For experiments, ask yourself: What are the “weak spots” that the experiment ought to probe? What information can we gather?
FCP decisions
FCP decisions represent a significant commitment from the team. They require sign-off from team members and follow a formal process for resolving concerns. An FCP decision cannot be reversed without another FCP—the expectation is that reversal requires some change in circumstances: new experience, new information, or a reasoned change of mind.
FCP decisions are used for stabilization, RFC approval, and other cases where we are making a promise to our users or taking a position we don’t want to reverse lightly.
Definitions
- Lang team: The Language Design Team.
- Lang team member: A member of the lang team. Lang team members are the ones who approve the final decision.
- Advisor: A member of the lang team advisors. Advisors can propose a decision and can raise concerns, but their approval is not required.
- Decision document: The RFC, issue text, or other document describing the decision being made.
- Document author: The person who authored the decision document. They ultimately decide what changes they wish to make in response to concerns.
The process
FCP decisions use rfcbot. They always begin with a decision document authored by the document author, who can be anyone—they do not have to be a member of a Rust team.
To start an FCP, a lang team member or advisor issues @rfcbot fcp merge (or fcp close for closing). This creates checkboxes. Once enough boxes have been checked (per rfcbot’s standard rules), the decision enters final-comment-period. Assuming no concerns are raised, the decision is finalized once the FCP has expired.
Before finalizing the decision, the team should engage with any new points raised during the FCP period, particularly from people not able to raise formal concerns.
Expected contents for an FCP
FCP proposals should include
- Motivation –
- Why is this change being made?
- If this is reverting a prior FCP, what aspects of the original rationale no longer apply?
- Details –
- What is the old/current behavior and what will be the new behavior?
- Design considerations –
- What alternatives did you consider? why did you choose this route over those?
- What are common misconceptions?
FCP proposals do not have to be long, but they should capture the key rationale backing the decision. It is often helpful to link to full details.
Blocking concerns
Lang team members and advisors can raise a blocking concern during the discussion process using @rfcbot concern concern-name. The decision cannot be finalized until the concern is resolved.
Before raising a concern
Before raising a blocking concern, we recommend that you nominate the issue for discussion in the triage meeting. Many blocking concerns can be resolved quickly.
Expectations when raising a concern
The person who raises a concern is expected to:
- Write a constructive comment explaining the concern;
- Make themselves available for discussion in a reasonable fashion;
- Be prepared to write a summary document if requested.
Resolving concerns
There are two ways to resolve a concern:
1. Withdrawn by the person who raised it
If the person who raised the concern is satisfied—whether because of changes made or because they’ve decided not to block progress—they resolve the concern themselves with @rfcbot resolve. This is the preferred outcome.
2. Resolution proposed
If the concern is not withdrawn, someone (e.g. the document author) can propose a resolution. This is a more formal process that ensures concerns are genuinely engaged with.
Creating a concern issue
For concerns that require extended discussion (judgment calls rather than simple technical corrections), create a GitHub issue to track the concern. In most cases — when the concern involves a change to the language — the issue will be filed in rust-lang/rust. Otherwise, if there is no other suitable repository, the issue can be filed on rust-lang/lang-team.
Note: We recommend that rfcbot be updated to create these issues automatically. Until then, create them manually.
The resolution template
A resolution must include:
-
Summary of the concern: Demonstrate understanding of the concern by summarizing its key points. The person who raised the concern should be able to confirm “yes, you understood me.”
-
Proposed changes: What changes (if any) are being made to the original proposal, and how they address specific points of the concern. This may be empty if no changes are being made.
-
Rationale: Explain the reasoning behind the resolution. If any aspects of the concern are not being addressed, explain why (typically because they conflict with other goals or constraints). Referencing the design axioms and their ordering can be a useful way to document this rationale.
FCP on the resolution
Post the resolution to the concern issue and start an FCP with @rfcbot fcp merge.
Blocking the resolution of a concern
Blocking a concern resolution is held to a higher standard than blocking the original decision:
- Only full lang-team members (not lang-team advisors) may raise a blocking concern on a concern resolution.
- When a lang-team member blocks the resolution of a concern, that concern must be seconded by another lang-team member.
- If a second cannot be found within a reasonable time, then the concern must be withdrawn. The typical procedure is to discuss the concern in the next triage meeting and to withdraw the concern if nobody seconds it at that time, but leads may opt to give more or less time depending on circumstances.
- Before the concern is withdrawn, the team member who raised it may request a design meeting. They are expected to author a document explaining why they feel the resolution of the concern is incorrect. At the end of this meeting, there will be a call for seconds from the team; if nobody agrees to second the concern, then it must be withdrawn.
- If the second is withdrawn, then another second must be found or else the concern must be withdrawn.
The rules are setup to ensure that a single lang-team member cannot block the remainder of the team if they are aligned on how the concern ought to be resolved.
Reasons to second a concern
There are two reasons to raise or second a concern raised on a resolution:
- You are not convinced that the resolution is the correct path:
- You may feel that the resolution does not adequately address the concern.
- You may feel that the resolution goes too far in addressing the concern and creates new issues of its own.
- You may not be sure what is right and prefer to take a more conservative path (e.g., erroring or stabilizing a more narrow set of behavior).
- You feel the original concern has not been sufficiently discussed:
- Sometimes you may not yet be convinced that the original concern is an issue, but you may feel that the champion has moved to resolve the concern too quickly, and that more discussion is warranted.
The team should be careful when opting not to second a concern raised on a resolution. If there is a “forwards compatible” option that preserves the possibility to defer the choice, that is usually better (unless that forwards compatible limitation fully prevents a primary use case).
After the resolution
If the resolution FCP completes successfully:
- If the resolution included changes to the original proposal, restart the original FCP to give people a chance to review the updated proposal.
- If no changes were made, the original FCP can continue.
History
- In April of 2026 the process was revised in PR #360. This included a significant discussion about how to manage blocking concerns with a dissent. See the documented rationale for that PR for more details.
- A later PR revised the process to a new rfcbot merge procedure that was never adopted in practice. This included the requirement that blocking concerns be “seconded”.
- The original consensus procedure was defined in RFC #1068, which specifies consensus with the team lead empowered to resolve cases where the team cannot come to consensus.
Becoming and being a lang-team member
Lang team members are the ones who ultimately decide the syntax/semantics of the Rust language. They judge whether a given piece of Rust syntax ought to compile and, if it does, what should happen when it runs.
Relations to other teams
Lang team members should be familiar with how the compiler works, but they don’t need to drive its implementation or even work on it. Compiler implementation details are the job of the compiler team. Note that compiler team members are encouraged to raise concerns if they feel that the desired design cannot be implemented in a reasonable and maintainable way.
Lang team members have to know the language inside and out, but they don’t necessarily need deep type theory background. Working out the formal semantics of the language as well as diving into very detailed questions is covered by the types team. Types team members are encouraged to raise concerns if they feel that the desired design cannot be made sound or is internally inconsistent.
Expectations
Lang team members are expected to
- Help to advance the state of the language by some combination of participating in discussions on RFCs and Zulip, authoring and editing RFCs, and shepherding features through the stabilization process.
- Attend triage and design meetings regularly.
- Promptly respond to RFC decisions by reviewing the question and either checking box or raising concerns
Process to add a new member
Lang team members can propose new additions to the team as follows:
- Lang team member prepares a short write-up to propose candidate to the rest of the team
- The write-up should draw on the qualifications below, giving examples where the candidate demonstrated the various criteria
- Leads check with moderation team for known flags, surface to team if any.
- The decision to add must have unanimous consent, similar to any other decision. Objections may be raised in a private team discussion, or by contacting a team lead.
Important: Discussions about potential new members are kept strictly confidential. Email or voice conversation are often preferred because they doesn’t keep publicly available records.
Qualifications
These are the questions we ask ourselves when deciding whether someone would be a good choice as a lang team member.
- Has this person demonstrated strong language design skills?
- Have they made notable contributions to an area of the language, such as leading an impactful initiative to completion?
- Are they able to identify flaws in a design and, just as importantly, come up with creative solutions?
- Is this person responsible?
- When they agree to take on a task, do they either get it done or identify that they are not able to follow through and ask for help?
- Is this person able to lead others to a productive conversation?
- Are there times when a conversation was stalled out and this person was able to step in and get the design discussion back on track?
- This could have been by suggesting a compromise, but it may also be by asking the right questions or encouraging the right tone.
- Are there times when a conversation was stalled out and this person was able to step in and get the design discussion back on track?
- Is this person able to disagree collaboratively, constructively, and with empathy?
- The expectation is that team members go “above and beyond” the Rust code of conduct, embodying not only the letter but also the spirit.
- Do they help turn disagreements into collaborations, jointly seeking a mutually satisfying solution based on everyone’s values?
- When they are having a debate, do they make an active effort to understand and repeat back others’ points of view?
- If they or others have a concern, do they engage actively to make sure it is understood and to look for ways to resolve it?
- Do they respect others when disagreeing, seek earnestly to understand others’ points of view, and show that they value others for bringing forward reasonable disagreement and dissent?
- Is this person active?
- Are they attending the triage meeting and design meetings regularly? (Meetings are open for anyone to attend, but note that merely attending meetings is not enough to become a team member!)
- Either in meeting or elsewhere, do they comment on disussions and otherwise?
- Does this person have an overall desire to improve the language, rather than a strong interest in some particular domain?
- Everyone have preferences, but members are responsible for balancing a wide array of interests. Someone with very specialized interest may be a better choice for a lang team advisor.
Keep in mind that qualifications are not a checklist and membership decisions are ultimately made on a case-by-case basis. If you are interested in joining the lang team, we recommend you reach out to the lead(s) to talk about the path forward.
Lang team leadership
Lang team leads drive the overall team agenda and tend to its well-being. They generally chair the meetings, keep an eye out for conflicts, and are to some extent the decision makers of last resort. They drive the roadmap process. They also post blog posts updating the public (possibly through delegation).
Process to select leads
We do not have a fixed point in time for selecting new leadership. However, when we opt to select new leadership, the process is as follows:
- Current lang team leads send out an email or message seeking nominations
- Lang team members (including the current leads) suggest candidates for new leads
- Members may nominate themselves.
- Leads may nominate to continue leading.
- Based on these nominations, leads propose the next set of leads and team members ratify the choice
- A private meeting (lang team members only) may be useful to discuss.
- It is generally recommended to pair one new co-lead with an older one who can advise.
- To avoid burnout, rotation of leadership is recommended.
Qualifications
Questions to ask when nominating or selecting a lead:
- Does this person have time to be a lang team lead?
- Are they widely respected amongst the team?
- Do they excel at resolving disputes, even amongst lang team members?
- All leads are lang team members, so they are are assumed to meet the general criteria for lang team membership, but typically leads excel at the question of leading others to productive discussion.
Chat platform
The lang team hangs out in the rust-lang Zulip in the #t-lang
stream. There are also other #t-lang/* streams that might be of
interest.
Calendar
The lang team has two recurring weekly meeting slots:
- Triage meetings: Weekly on Wednesdays at 11:00 U.S. Eastern Time (convert to local time).
- Design/planning meetings: Weekly on Wednesdays at 12:30 U.S. Eastern Time (convert to local time).
When adjusting for other time zones, note that our meeting times follow changes in the particulars of U.S. daylight saving time.
For the design/planning meeting slot, we typically do a planning meeting on the first Wednesday of the month, then we do design meetings on each remaining Wednesday.
Our meetings are open to all those who are interested to listen. You can join in at: https://meet.jit.si/ferris-rules.
To subscribe to the calendar of our recurring meetings, add this URL to your favorite calendaring application:
Our schedule of upcoming design meetings is tracked in a GitHub project and can be viewed here:
Each design meeting issue includes details about what we’ll be discussing. After the meeting, the issue will include a link to the minutes.
Minutes for all of our recent meetings can be found here:
Periodically, we archive our meeting minutes to git:
Subteam Calendars
Some subteams of the lang team reuse the primary lang team calendar for their own calendar events, while others may choose to create a separate calendar. We’ll list below the calendars of all subteams that do not use the main calendar.
Meetings
The lang team has several kinds of standing meetings.
Unless otherwise noted, all of our meetings are open to the public for anyone to attend. You will find the timing and event details on our lang team calendar. We publish notes and minutes in written form in this github repository.
Recording policy
Dy default, our triage and design meetings are not recorded, in order to encourage engagement from a broad audience. We may record certain design meetings, evaluated on a case-by-case basis, and only with the agreement of all participants. Any intention to record a design meeting will be established at that time that meeting is scheduled during the monthly planning meeting and included in the blog post announcing the upcoming meetings.
Our YouTube playlist has recordings of some of our past meetings (along with automatically created subtitles).
Triage meeting
The weekly triage meeting is when we go over the newly filed project proposals along with issues that have been nominated for lang-team feedback. We also get regular updates from the active project groups so we can stay on top of what is going on.
Can I attend?
Yes! The triage meeting is open to the public. You’ll find the details on our calendar.
How do I get something on the agenda?
The easiest way to get something on the agenda is to nominate it. The agenda is automatically built by triagebot from this template. We review pending initiative proposals, nominated issues and PRs from a variety of repositories, as well as pending RFC requests.
Can I generate the agenda myself?
Sure. You can visit the following link and one will be generated for you for (the current date):
https://triage.rust-lang.org/agenda/lang/triage
Alternatively, clone the triagebot repo and run this
> cargo run --bin lang agenda
If you install the hackmd-cli, you can do this:
cargo run --bin lang agenda | hackmd-cli import
Where can I find the minutes?
Triage meeting minutes are available in this directory.
Design meetings
We reserve a weekly slot for our planning and design meetings. A design meeting is a one-hour deep-dive discussion into some particular topic. Each meeting is centered around a document that is prepared for that meeting explaining the details of what is to be discussed; we begin by reading the document and then discussing its contents. These meetings are used for all kinds of purposes, such as brainstorming, getting feedback on an idea, or building consensus around a specific proposals.
How are design meetings scheduled?
To schedule design meetings, we hold a special planning meeting once per month. In that meeting, we choose what design meetings we will hold the rest of the month.
To generate the agenda for the planning meeting, you can use the following link and then copy/paste the generated text into a fresh hackmd page:
https://triage.rust-lang.org/agenda/lang/planning
How do I propose a design meeting?
You need to open an issue, as described here.
Can I attend?
Yes! Design meetings are open to the public. You’ll find the details on our calendar and a list of the upcoming meetings in the design meeting schedule.
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.
Frequently Requested Changes
Some ideas for language proposals come up quite often. They’re attractive ideas for one reason or another, but ones that we’re unlikely to add.
This page documents some of those ideas, along with the concerns that argue against them.
If something appears on this page, that doesn’t mean Rust would never consider making any similar change. It does mean that any hypothetical successful proposal to do so would need to address at a minimum all of these known concerns, whether by proposing a new and previously unseen approach that avoids all the concerns, or by making an extraordinary case for why their proposal outweighs those concerns.
Hopeful proposers of any of these ideas should document their extensive research into the many past discussions on these topics and ensure they have something new to offer.
An operator for unwrap
People writing code that makes extensive use of .unwrap() often ask for a
shorthand operator for it, typically something postfix involving !.
Rust already provides the ? operator for propagating errors. .unwrap()
exists largely for quick-and-dirty code. We don’t want to make it substantially
easier than it already is to write code using .unwrap(), and we definitely
don’t want to add dedicated syntax for it.
An option to disable the borrow checker, or bypass it in unsafe code
People learning Rust, especially those arriving from other languages, often spend time “fighting the borrow checker”. And experienced developers sometimes want to write “clever” code that the borrow checker doesn’t understand.
In the course of doing so, some developers request a compiler option to
“disable the borrow checker”, or a way to bypass the borrow checker in unsafe
code blocks.
Rust already provides a means of bypassing the borrow checker: you can write
unsafe code that uses “raw pointers” (*const T or *mut T, rather than
&T or &mut T). Using raw pointers, you can manipulate memory in any way you
see fit, and Rust’s borrow checker will do nothing to stop you; if you misuse
raw pointers, you’ll get crashes or incorrect behavior at runtime. Many safe
Rust data structures and libraries are safe wrappers designed to encapsulate
some carefully written unsafe code.
You can also defer borrow checking to runtime, with types like RefCell, Rc,
and Arc. These types also allow “interior mutability”: modifying a value
whose type doesn’t look modifiable, when the value isn’t “semantically” being
modified. (For instance, managing mutable internal bookkeeping for an otherwise
immutable value.)
However, even in an unsafe block, Rust’s normal borrowed types (&T and
&mut T) still follow the same rules they do everywhere else. You can’t have
two mutable references to the same object at once; you can’t have a mutable
reference and an immutable reference at the same time; you can’t use an object
after giving away ownership of it. Having distinct types for safe borrows and
unsafe raw pointers provides the flexibility of writing unsafe code while still
getting support from the compiler in places where your code can benefit from
such support.
Fundamental changes to Rust syntax
This includes proposals such as changing the generic syntax to not use </>,
changing block constructs to not require braces, changing function calls to not
require parentheses, and many other similar proposals. These also include
proposals to add “alternative” syntaxes, in addition to those that replace the
existing syntax. Many of these proposals come from people who also write other
languages. Arguments range from the ergonomic (“I don’t want to type this”) to
the aesthetic (“I don’t like how this looks”).
Changes that would break existing Rust code are non-starters. Even in an edition, changes this fundamental remain extremely unlikely. The established Rust community with knowledge of existing Rust syntax has a great deal of value, and to be considered, a syntax change proposal would have to be not just better, but so wildly better as to overcome the massive downside of switching.
In addition, such changes often go against one or more other aspects of Rust’s design philosophy. For instance, we don’t want to make changes that make code easier to write but harder to read, or changes that make code more error-prone to modify and maintain.
That said, we are open to proposals that involve new syntax, especially for new features, or to improve an existing fundamental feature. The bar for new syntax (e.g. new operators) is high, but not insurmountable. But the bar for changes to existing syntax is even higher.
Changes to avoid writing self.method() when calling a method from a method
In Rust, within a method of an object, calling another method requires writing
self.othermethod(), and accessing a field requires self.field. In some
other languages, such accesses can omit the equivalent of self., and just
write othermethod or field, implicitly referencing the “current object”.
People writing code with such method calls or field accesses sometimes ask for
such shorthand in Rust.
Rust prefers the explicitness of writing self. for method calls and field
accesses, and in most ways treating self as a normal object of the type. This
avoids unexpected calls to the wrong method, makes it easier to distinguish
methods and free functions, and makes code easier to read. We don’t want to
provide a shorthand that makes this syntax briefer at the expense of this
clarity and unambiguity.
Arbitrary custom operator syntax
Rust allows overloading existing operators; for instance, you can implement the
Add trait to overload the + operator. However, Rust does not allow creating
new operators.
Some would argue this can make code more readable, if you know what the operators mean. If you don’t, it makes the code inscrutable.
In general, such a change would substantially raise barriers to entry for Rust developers, making Rust code less approachable and less universally understandable. We’re unlikely to add support for this.
Note that Rust’s existing operator overloading uses semantic trait names (impl Add), rather than symbols or names of symbols (Plus or +), which tends to
encourage using overloaded operators for the same semantic purposes, rather
than for building arbitrary domain-specific languages.
Rust developers seeking to build arbitrary domain-specific languages (DSLs) should consider the macro system.
Numeric overflow checking should be on by default even in release mode
Whenever possible, Rust tries to do the safe thing by default.
Numeric overflow checking (e.g. 1000u16 * 1000u16) is one case where Rust
compromised on this: on many targets, numeric overflow checking has high enough
overhead to hurt performance too much for a wide variety of code. As a result,
Rust defaults to having overflow checking only for debug builds, while release
builds have overflow checking off by default. (In release builds, numeric
overflow wraps, but code cannot count on overflow checking being disabled even
in release builds, as projects can turn on overflow checking in release builds.
In addition, library code cannot make any assumptions about overflow checking,
as the top-level compilation decides whether to enable or disable it.)
We’ve thought about this choice many times, and we’re open to considering changes to this default based on benchmarks. If, on some Rust targets, overflow checking adds fairly little overhead on the vast majority of crates, we’d consider enabling it by default for those targets.
It would also help to have ways to detect excessive overhead caused by overflow
checking (e.g. detecting numeric-heavy code) and suggesting the use of
explicitly non-overflowing numeric types such as Wrapping.
Cross-function type inference
Rust’s type inference generally stops at function boundaries; Rust requires specifying explicit types for function parameters, rather than allowing inference to work across functions.
This is an intentional design choice: by making functions an inference boundary, type errors become easier to debug and compartmentalize, and Rust developers can reason about code using local reasoning within a function.
Built-in / mandatory garbage collection
Adding any form of mandatory garbage collection built into the language would mandate that all targets support it, which would require some kind of “runtime”. Rust gets great benefit from having no required “runtime”. Rust can go anywhere, including in systems contexts where relatively few languages can.
That said, we’re happy to add language features to support an optional garbage collector, where needed.
Suffix modifiers (if after return/break/continue, or after arbitrary statements)
We often get proposals for syntax like return expr if condition; or
break if condition;. We don’t plan to make such a change to Rust.
Such a change would prioritize concise code over readable code. We don’t
want people to start out thinking they’re reading an unconditional return
statement, and only later see the if and realize it’s a conditional return.
Such a change would also have non-obvious evaluation order (evaluating the condition before the return expression).
Size != Stride
Rust assumes that the size of an object is equivalent to the stride of an object -
this means that the size of [T; N] is N * std::mem::size_of::<T>. Allowing
size to not equal stride may allow objects that take up less space in arrays due
to the reuse of tail padding, and allow interop with other languages with this behavior.
One downside of this assumption is that types with alignment greater than their size can waste large amounts of space due to padding. An overaligned struct such as the following:
#[repr(C, align(512))]
struct Overaligned(u8);
will store only 1 byte of data, but will have 511 bytes of tail padding for a total size of
512 bytes. This tail padding will not be reusable, and adding Overaligned as a struct field
may exacerbate this waste as additional trailing padding be included after any other members.
Rust makes several guarantees that make supporting size != stride difficult in the general case.
The combination of std::array::from_ref and array indexing is a stable guarantee that a pointer
(or reference) to a type is convertible to a pointer to a 1-array of that type, and vice versa.
Such a change could also pose problems for existing unsafe code, which may assume that pointers can be manually offset by the size of the type to access the next array element. Unsafe code may also assume that overwriting trailing padding is allowed, which would conflict with the repurposing of such padding for data storage.
While changing the fundamental layout guarantees seems unlikely, it may be reasonable to add additional
inspection APIs for code that wishes to opt into the possibility of copying smaller parts of an object
– an API to find out that copying only bytes 0..1 of Overaligned is sufficient might still be
reasonable, or something size_of_val-like that could be variant-aware to say which bytes are sufficient
for copying a particular instance. Similarly, move-only fields may allow users to mitigate the effects
of tail or internal padding, as they can be reused due to the lack of a possible reference or pointer.
Cross-referencing to other discussions:
- https://github.com/rust-lang/rfcs/issues/1397
- https://github.com/rust-lang/rust/issues/17027
- https://github.com/rust-lang/unsafe-code-guidelines/issues/176
Design notes
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:
- Floating point literals are prone to many surprises that integers
are not. For example,
20_000_000 + 1, if inferred tof32type, would have the final value20_000_000.0. This leads some to conclude that “the surprise factor of floats is so high that they are qualitatively different than integers”. - But there are a lot of similar surprising things:
20_000_001_f32, for example, is also going to be20_000_000.- Integer overflow can mean that
255 + 1has the value 0.
- Some key questions to consider:
- How often does adding
.0result in some insight? - How much would seeing the
.0help to debug a tricky problem?
- How often does adding
- Balanced against the annoyance and surprise factor.
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
IteratororStreamwhile 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
Generatortrait. - 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 newFnPinfor immovable coroutines.- Hierarchy:
FnisFnMut + UnpinisFnPinisFnOnce.- 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.
- May not be required at the trait level (someone may someday find a use
to implementing
- Hierarchy:
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”.
- 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
ctxshadows 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
yieldexpressions. - This part of why it is so hard to unify generalized coroutines with a
generator syntax like
gen { }orgen fn. Where does the first input go? Where do you annotate the argument type even?
- eRFC-2033 passes the first resume argument via a closure parameters
while later arguments are produced by
- 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.
- Also get multiple arguments for free if using the
- 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.
- 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.
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
xis 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
LendingStreamandLendingIteratortraits in “future additions”. - We should probably get some experience with lending traits at the lib level before attempting to add language level support.
- RFC-2996 gets the closest with a mention of
- 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 xshould produceGeneratorState::Yielded(x)or equivalent as an output, in order to discriminate between yielded and returned values. - MCP-49 instead gives
yield xandreturn xnearly identical semantics and outputxdirectly, 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 (
()vsT) 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
returnshould 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 returnTby some sort of combinator and a coroutine that only returnsTcan haveyieldandreturnvalues manually wrapped inGeneratorState. 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 Unpinto 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.
- If we forbid “borrowed data escaping into closure state”, the inference
rules should be relatively simple: witnessing any borrow triggers
immovability.
- 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
!Unpinbecause of inference rules - If the pinned data is
!Unpinand is witnessed ⇒ coroutine is!Unpinbecause witness contains!Unpindata - Thus, if the coroutine can be moved after resume, any data stack-pinned
(really witness-pinned) by
pin_mut!is not referenced and isUnpin.
- If the resulting mutable borrow is witnessed ⇒ coroutine is
- Until inference is solved, the
statickeyword 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
FnOnceclosures, 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.
- Attempts to discriminate with enums tend to run up against
- 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.
- Following
- 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.
- This can be very handy in some situations. In fact, I use it several times
in examples to increase readability. See anywhere I
- MCP-49 also optionally proposes that capture-destroying closures should
only implement
FnOnceunless explicitly annotated, even if they should apparently be resumable several times.mut || { drop(capture); }is recommended as the modifier, to hint that anFnMutimpl is being requested when the closure in question would otherwise impl onlyFnOnce.- 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.
- In the context of MCP-49, how should
- Async coroutines don’t make much sense because of resume arguments. Async
functions are already coroutines which take an
&mut Contextas 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
Pendingyields. - 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?
- Do the additional args need to be passed every single poll or are they only
needed when resuming after
- 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 implementingawaitloops on arbitraryPoll-returning functions.- Would be a good candidate for an
.await(args..)syntax if very heavily used.
- Would be a good candidate for an
- For example, an simple little checksumming async write wrapper might look like this:
- An
|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<_, _>>andPoll<Option<Result<_, _>>>already implementTry!- Generators usually want a totally different
?desugar that doesyield Some(Err(...)); return None;instead ofreturn 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
yieldis involved, some variables get stored in a witness struct rather than in the stack frame. Plus the need for a poison state.
- In fact,
returnbehaves exactly like a simultaneousyield+break 'closure_body.- In a sense, every closure already has a single yield point at which it
resumes after
return. - A
yieldadds a second resume point: hence the need for a discriminant.
- In a sense, every closure already has a single yield point at which it
resumes after
- 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 implementIteratororStreamwould cause confusion. - On that note, generators definitely want to implement different traits vs
coroutines.
IteratorandStreamrather thanFnor (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
genandgen_asyncmacros 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,FnMutandFnOncetraits are implemented based on the way in which upvars are used. -
CopyandClonetraits are implemented when all upvars implement the same trait (trivially true for function types without upvars). -
autotraits 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}: Defaultis 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
Clonetrait implementation. A function author may write aFnOnceclosure and rely on it only being callable once. However, if the upvars are allClonethen 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
Defaultbound would have to be explicitly listed which would then be readily visible to consumers of the upstream code. -
Features like
impl Traitmake it relatively easy to avoid leaking this capability when it’s not wanted.
Points for this argument:
- The
Clonetrait requires an existing instance of the function in order to be exploited. The fact that theDefaulttrait gives this capability to types directly makes it sufficiently different fromCloneto 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
Defaultas 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
Defaultis not sufficient to guarantee uniqueness of the function value. Code could be written today that exposes a public API with aDefault + 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 stabilizingFntrait implementations or by making more function types implementDefault) 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
Fntrait 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
- Project proposal rust-lang/lang-team#86
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:
{
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 effectlet _ = foo()– immediately drops the result of invokingfoo()let _guard = foo– movesfoointo_guardand drops at the end of the blocklet _guard = foo()– movesfoo()into_guardand 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.
- Note that the scopeguard crate offers macros like
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 + &Tis essentially equivalent at runtime toT + 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:
- Rust tracking issue #44762
- This includes some implementation/mentoring notes.
- RFC 2147
- Trying to add T op= &T
- Showcases dealing with inference breakage regressions when adding new reference-taking impls.
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 Twhich avoids the requirement that T is recursively Copy, but is obviously unsafe.- https://github.com/rust-lang/rust/issues/25053#issuecomment-218610508
Copyis dangerous on types likeUnsafeCellwhere&UnsafeCell<T>otherwise would not permit access toTin 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
- Proposed lint: #45683
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
- RFC 2111 (not merged)
- Rust tracking issue (closed)
- “Allow owned values where references are expected” in rust-roadmap-2017#17
Rationale
This section captures the motivation and rationale for major changes to our procedure. These records take the rough shape of a Rust RFC but do not have to follow the template in a strict fashion.
PR #360 Rationale
This documents the changes made in PR #360:
- The role of the champion is better defined:
- Champions are drawn from lang-team and lang-team advisors
- Champions drive experiments and have the power to make reversible decisions
- Champions are expected to keep the lang-team abreast of updates and decisions and to bottom out lang-team concerns
- The formal decision making process is modified:
- FCP decisions are made by the team
- Blocking concerns can be raised by lang-team members and advisors
- Blocking concerns can be resolved in two ways:
- the person who raised the concern may opt to withdraw it
- a FCP on a resolution document that proposes a resolution
- This resolution document can be sponsored by any team member.
- Blocking this resolution document requires two lang-team members (not advisors). This ensures that a single lang-team member cannot block the rest of the team (but two can).
- The ideal size of the team is defined as 4-8 members.
Changes from the past include:
- Older material concerning an alternative rfcbot procedure is removed, as it is has not been enacted (we may reconsider it at a future date).
- There is now a clearer path for resolving concerns; in the past, the procedure said that concerns had to be “seconded” but the means of doing so was ill-defined.
Motivation
The motivation for these changes was to increase individual autonomomy. We wish for the lang team to retain its role of shaping and vetting language features but we wish to ensure that the champions and owners driving a particular feature feel empowered to make forward progress.
Design axioms
- Perfection is a process. We do our best to get designs right but also recognize the limits of deliberation. Many design constraints only become apparent (or relevant) once a feature has been adopted. For complex designs, we prefer to ship incrementally, addressing the most important needs first and using the experience gained to inform future design work.
- Treasure dissent. The best designs arise out of finding a creative way to overcome two seemingly incompatible constraints. When someone raises a concern that blocks your progress, you should look on it as an opportunity to improve your design so it can meet more needs.
Frequently Asked Questions
Why are Champion Decisions “unblockable”?
In the process as written, Champion Decisions (e.g., to start or stop an experiment) cannot be blocked by members of the lang-team. We wish to lean in favor of autonomy and easy experimentation and avoid the perception that champions need to wait for the lang-team to “weigh in” before making lightweight decisions (e.g., how to focus their time). The intended flow is rather that champions document those decisions for periodic review by the lang-team after the fact. Any concerns held my members of the team can be raised there; champions would do well to heed them, as those concerns will simply arise later during the FCP process.
Why do we require two lang-team members to block a concern resolution?
The rationale and discussion was captured in resolution issue #365. That decision framed it as a question of “recursive blocking” – can the same person who raised a concern also block resolving the concern. The decision in the issue was “no, they cannot”, which favors “perfection is a process” over “treasure dissent”. This decision was not unaninmous and one team member opted to include a dissent (included below).
The final language does not describe “recursive blocking” but instead that blocking the resolution of a concern must be seconded. This was intended to allow for the fact that advisors may raise the original blocking concern, at which point the definition of a “recursive blocking” was unclear.
We also added process scaffolding that encourages genuine engagement with concerns, particularly in cases where the concern-raiser does not sign off on the resolution:
- Template for resolution write-ups, including specific reflection questions (see the FAQ for details)
- The option for the concern-raiser to explain why they feel the resolution is not sufficient in a design meeting.
Dissent by Josh Triplett
Josh Tripplet did not agree with the decision to remove recursive blocking and wished to record a dissent. This spot is left for him to add the dissent in a future PR.
Why did we consider removing the ability for advisors to raise a blocking concern?
Another option to resolve the inconsistency between recursive blocking and advisors raising the initial concern was to remove the ability for advisors to raise concerns. Initially “able to raise concerns” was the primary power granted to advisors, but in our new design advisors primary role is to serve as champions, and hence it was unclear whether they should continue to be able to raise concerns. Ultimately we opted to allow them to raise concerns but to require that blocking of resolutions be seconded. This is a smaller change to our procedure and allows for the idea that many advisors are deep experts in a particular slice of the language and they should be able to raise concerns in those particular areas.