Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the recorded types in MIR to determine generator auto-trait implementations #65782

Closed
wants to merge 15 commits into from

Conversation

Aaron1011
Copy link
Member

@Aaron1011 Aaron1011 commented Oct 24, 2019

See #57017

When we construct a generator type, we build a ty::GeneratorWitness
from a list of types that may live across a suspend point. This types is
used to determine the 'constitutent types' for a generator when
selecting an auto-trait predicate. Any types appearing in the
GeneratorWitness are required to implement the auto trait (e.g. Send
or Sync).

This analysis
is based on the HIR - as a result, it is unable to take liveness of
variables into account. This often results in unecessary bounds being
computing (e.g requiring that a Rc be Sync even if it is dropped
before a suspend point), making generators and async fns much less
ergonomic to write.

This commit uses the generator MIR to determine the actual 'constituent
types' of a generator. Specifically, a type in the generator witness is
considered to be a 'constituent type' of the witness if it appears in
the field_tys of the computed GeneratorLayout. Any type which is
stored across an suspend point must be a constituent type (since it could
be used again after a suspend), while any type that is not stored across
a suspend point cannot possible be a consituent type (since it is
impossible for it to be used again).

By re-using the existing generator layout computation logic, we get some
nice properties for free:

  • Types which are dead before the suspend point are not considered
    constituent types
  • Types without Drop impls not considered consitutent types if their
    scope extends across an await point (assuming that they are never used
    after an await point).

Note that this only affects ty::GeneratorWitness, not
ty::Generator itself. Upvars (captured types from the parent scope)
are considered to be constituent types of the base ty::Generator, not
the inner ty::GeneratorWitness. This means that upvars are always
considered constituent types - this is because by defintion, they always
live across the first implicit suspend point.


Implementation:

The most significant part of this PR is the introduction of a new
'delayed generator witness mode' to TraitEngine. As @nikomatsakis
pointed out, attmepting to compute generator MIR during type-checking
results in the following cycle:

  1. We attempt to type-check a generator's parent function
  2. During type checking of the parent function, we record a
    predicate of the form <generator>: AutoTrait
  3. We add this predicate to a TraitEngine, and attempt to fulfill it.
  4. When we atempt to select the predicate, we attempt to compute the MIR
    for <generator>
  5. The MIR query attempts to compute type_of(generator_def_id), which
    results in us attempting to type-check the generator's parent function.

To break this cycle, we defer processing of all auto-trait predicates
involving ty::GeneratorWitness. These predicates are recorded in the
TypeckTables for the parent function. During MIR type-checking of the
parent function, we actually attempt to fulfill these predicates,
reporting any errors that occur.

The rest of the PR is mostly fallout from this change:

  • ty::GeneratorWitness now stores the DefId of its generator. This
    allows us to retrieve the MIR for the generator when SelectionContext
    processes a predicate involving a ty::GeneratorWitness
  • Since we now store PredicateObligations in TypeckTables, several
    different types have now become RustcEncodable/RustcDecodable. These
    are purely mechanical changes (adding new #[derives]), with one
    exception - a new SpecializedDecoder impl for List<Predicate>.
    This was essentialy identical to other SpecializedDecoder imps, but it
    would be good to have someone check it over.
  • When we delay processing of a Predicate, we move it from one
    InferCtxt to another. This requires us to prevent any inference
    variables from leaking out from the first InferCtxt - if used in
    another InferCtxt, they will either be non-existent or refer to the
    the wrong variable. Fortunately, the predicate itself has no region
    variables - the ty::GeneratorWitness has only late-bound regions,
    while auto-traits have no generic parameters whatsoever.

However, we still need to deal with the ObligationCause stored by the
PredicateObligation. An ObligationCause (or a nested cause) may have
any number of region variables stored inside it (e.g. from stored
types). Luckily, ObligationCause is only uesd for error reporting, so
we can safely erase all regions variables from it, without affecting the
actual processing of the obligation.

To accomplish this, I took the somewhat unusual approach of implementing
TypeFoldable for ObligationCause, but did not change the TypeFoldable
implementation of Obligation to fold its contained
ObligationCause. Other than this one odd case, all other callers of TypeFoldablehave no interest in folding anObligationCause. As a result, we explicitly fold the ObligationCausewhen computing our deferred generator witness predicates. SinceObligationCause` is only
used for displaying error messages, the worst that can happen is that a
slightly odd error message is displayed to a user.

With this change, several tests now have fewer errors than they did
previously, due to the improved generator analysis. Unfortunately, this
does not resolve issue #64960. The MIR generator transformation stores
format temporaries in the generator, due to the fact that the format!
macros takes a refernece to them. As a result, they are still considered
constituent types of the GeneratorWitness, and are still required to
implement Send and `Sync.

@rust-highfive
Copy link
Collaborator

r? @varkor

(rust_highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Oct 24, 2019
@Aaron1011
Copy link
Member Author

I've filed a separate PR for making all generators Freeze in #65783

@rust-highfive
Copy link
Collaborator

The job x86_64-gnu-llvm-6.0 of your PR failed (pretty log, raw log). Through arcane magic we have determined that the following fragments from the build log may contain information about the problem.

Click to expand the log.
2019-10-24T23:04:12.7139300Z ##[command]git remote add origin https://github.com/rust-lang/rust
2019-10-24T23:04:12.7344310Z ##[command]git config gc.auto 0
2019-10-24T23:04:13.5429017Z ##[command]git config --get-all http.https://github.com/rust-lang/rust.extraheader
2019-10-24T23:04:13.5432628Z ##[command]git config --get-all http.proxy
2019-10-24T23:04:13.5435532Z ##[command]git -c http.extraheader="AUTHORIZATION: basic ***" fetch --force --tags --prune --progress --no-recurse-submodules --depth=2 origin +refs/heads/*:refs/remotes/origin/* +refs/pull/65782/merge:refs/remotes/pull/65782/merge
---
2019-10-24T23:14:58.4722747Z    Compiling rustc_errors v0.0.0 (/checkout/src/librustc_errors)
2019-10-24T23:15:17.4394458Z    Compiling fmt_macros v0.0.0 (/checkout/src/libfmt_macros)
2019-10-24T23:15:29.5610174Z    Compiling syntax_expand v0.0.0 (/checkout/src/libsyntax_expand)
2019-10-24T23:16:44.9134666Z    Compiling syntax_ext v0.0.0 (/checkout/src/libsyntax_ext)
2019-10-24T23:17:05.4211463Z error[E0004]: non-exhaustive patterns: `&Coercion { .. }` not covered
2019-10-24T23:17:05.4211833Z    --> src/librustc/macros.rs:393:53
2019-10-24T23:17:05.4212125Z     |
2019-10-24T23:17:05.4212440Z 382 | / macro_rules! EnumTypeFoldableImpl {
2019-10-24T23:17:05.4212776Z 383 | |     (impl<$($p:tt),*> TypeFoldable<$tcx:tt> for $s:path {
2019-10-24T23:17:05.4213100Z 384 | |         $($variants:tt)*
2019-10-24T23:17:05.4213677Z 385 | |     } $(where $($wc:tt)*)*) => {
2019-10-24T23:17:05.4213990Z ...   |
2019-10-24T23:17:05.4214379Z 393 | |                 EnumTypeFoldableImpl!(@FoldVariants(self, folder) input($($variants)*) output())
2019-10-24T23:17:05.4214942Z     | |                                                     ^^^^ pattern `&Coercion { .. }` not covered
2019-10-24T23:17:05.4215499Z 510 | |     };
2019-10-24T23:17:05.4215789Z 511 | | }
2019-10-24T23:17:05.4215789Z 511 | | }
2019-10-24T23:17:05.4216115Z     | |_- in this expansion of `EnumTypeFoldableImpl!`
2019-10-24T23:17:05.4216608Z    ::: src/librustc/traits/mod.rs:175:1
2019-10-24T23:17:05.4216826Z     |
2019-10-24T23:17:05.4216826Z     |
2019-10-24T23:17:05.4217140Z 175 | / pub enum ObligationCauseCode<'tcx> {
2019-10-24T23:17:05.4217491Z 176 | |     /// Not well classified or should be obvious from the span.
2019-10-24T23:17:05.4217793Z 177 | |     MiscObligation,
2019-10-24T23:17:05.4218315Z ...   |
2019-10-24T23:17:05.4218315Z ...   |
2019-10-24T23:17:05.4218648Z 205 | |     Coercion { source: Ty<'tcx>, target: Ty<'tcx> },
2019-10-24T23:17:05.4218972Z     | |     -------- not covered
2019-10-24T23:17:05.4220179Z 286 | |     TrivialBound,
2019-10-24T23:17:05.4221649Z 287 | | }
2019-10-24T23:17:05.4221649Z 287 | | }
2019-10-24T23:17:05.4222027Z     | |_- `traits::ObligationCauseCode<'tcx>` defined here
2019-10-24T23:17:05.4222240Z ...
2019-10-24T23:17:05.4222563Z 293 | / EnumTypeFoldableImpl! {
2019-10-24T23:17:05.4222910Z 294 | |     impl<'tcx> TypeFoldable<'tcx> for ObligationCauseCode<'tcx> {
2019-10-24T23:17:05.4224008Z 295 | |         (ObligationCauseCode::MiscObligation),
2019-10-24T23:17:05.4224486Z 296 | |         (ObligationCauseCode::SliceOrArrayElem),
2019-10-24T23:17:05.4225011Z 339 | |     }
2019-10-24T23:17:05.4225306Z 340 | | }
2019-10-24T23:17:05.4225779Z     | |_- in this macro invocation
2019-10-24T23:17:05.4226055Z     |
2019-10-24T23:17:05.4226055Z     |
2019-10-24T23:17:05.4226407Z     = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
2019-10-24T23:17:05.4226547Z 
2019-10-24T23:17:06.3775371Z error[E0004]: non-exhaustive patterns: `&Coercion { .. }` not covered
2019-10-24T23:17:06.3775733Z    --> src/librustc/macros.rs:400:54
2019-10-24T23:17:06.3775959Z     |
2019-10-24T23:17:06.3776284Z 382 | / macro_rules! EnumTypeFoldableImpl {
2019-10-24T23:17:06.3776623Z 383 | |     (impl<$($p:tt),*> TypeFoldable<$tcx:tt> for $s:path {
2019-10-24T23:17:06.3776955Z 384 | |         $($variants:tt)*
2019-10-24T23:17:06.3777295Z 385 | |     } $(where $($wc:tt)*)*) => {
2019-10-24T23:17:06.3777539Z ...   |
2019-10-24T23:17:06.3777918Z 400 | |                 EnumTypeFoldableImpl!(@VisitVariants(self, visitor) input($($variants)*) output())
2019-10-24T23:17:06.3778333Z     | |                                                      ^^^^ pattern `&Coercion { .. }` not covered
2019-10-24T23:17:06.3778896Z 510 | |     };
2019-10-24T23:17:06.3779188Z 511 | | }
2019-10-24T23:17:06.3779188Z 511 | | }
2019-10-24T23:17:06.3779506Z     | |_- in this expansion of `EnumTypeFoldableImpl!`
2019-10-24T23:17:06.3780397Z    ::: src/librustc/traits/mod.rs:175:1
2019-10-24T23:17:06.3780630Z     |
2019-10-24T23:17:06.3780630Z     |
2019-10-24T23:17:06.3780940Z 175 | / pub enum ObligationCauseCode<'tcx> {
2019-10-24T23:17:06.3781274Z 176 | |     /// Not well classified or should be obvious from the span.
2019-10-24T23:17:06.3781608Z 177 | |     MiscObligation,
2019-10-24T23:17:06.3782113Z ...   |
2019-10-24T23:17:06.3782113Z ...   |
2019-10-24T23:17:06.3782455Z 205 | |     Coercion { source: Ty<'tcx>, target: Ty<'tcx> },
2019-10-24T23:17:06.3782792Z     | |     -------- not covered
2019-10-24T23:17:06.3783350Z 286 | |     TrivialBound,
2019-10-24T23:17:06.3783633Z 287 | | }
2019-10-24T23:17:06.3783633Z 287 | | }
2019-10-24T23:17:06.3784329Z     | |_- `traits::ObligationCauseCode<'tcx>` defined here
2019-10-24T23:17:06.3784615Z ...
2019-10-24T23:17:06.3784906Z 293 | / EnumTypeFoldableImpl! {
2019-10-24T23:17:06.3785422Z 294 | |     impl<'tcx> TypeFoldable<'tcx> for ObligationCauseCode<'tcx> {
2019-10-24T23:17:06.3785750Z 295 | |         (ObligationCauseCode::MiscObligation),
2019-10-24T23:17:06.3786074Z 296 | |         (ObligationCauseCode::SliceOrArrayElem),
2019-10-24T23:17:06.3786630Z 339 | |     }
2019-10-24T23:17:06.3786907Z 340 | | }
2019-10-24T23:17:06.3787220Z     | |_- in this macro invocation
2019-10-24T23:17:06.3787449Z     |
---
2019-10-24T23:17:29.2891717Z   local time: Thu Oct 24 23:17:29 UTC 2019
2019-10-24T23:17:29.9043523Z   network time: Thu, 24 Oct 2019 23:17:29 GMT
2019-10-24T23:17:29.9046432Z == end clock drift check ==
2019-10-24T23:17:30.5727153Z 
2019-10-24T23:17:30.5840717Z ##[error]Bash exited with code '1'.
2019-10-24T23:17:30.5873072Z ##[section]Starting: Checkout
2019-10-24T23:17:30.5874986Z ==============================================================================
2019-10-24T23:17:30.5875060Z Task         : Get sources
2019-10-24T23:17:30.5875123Z Description  : Get sources from a repository. Supports Git, TfsVC, and SVN repositories.

I'm a bot! I can only do what humans tell me to, so if this was not helpful or you have suggestions for improvements, please ping or otherwise contact @TimNN. (Feature Requests)

@Aaron1011 Aaron1011 force-pushed the generator-mir branch 2 times, most recently from 4b1e618 to 58c81c2 Compare October 24, 2019 23:32
@Aaron1011
Copy link
Member Author

Aaron1011 commented Oct 25, 2019

From PR #65783:

@Zoxc:

The motivation for this PR is to support further work on #57017. My
approach is to delay resolution of auto-trait predicates (e.g.
{generator}: Send) until after we've constructed the generator MIR
(specifically after we've run the StateTransform MIR transformation
pass).

You definitely shouldn't be doing that. StateTransform should run after MIR optimizations and we can't have {generator}: Send) depend on optimizations.

But StateTransform (and all of the passes before it) are always run. So, this can't cause the Send/Sync-ness of a generator to depend on the overall program optimization level.

Are you concerned that these optimization passes will have some effect on the calculation of GeneratorLayout by StateTransform, which will in turn affect the auto-trait impls of the generator?

If so, I see two possibilities:

  1. An optimization pass causes a type to live across a suspend point which previously did not. I don't see how this could happen - if it did, I think we would want to consider it a bug, since at a minimum it would increase the size of the generator.

  2. An optimization pass causes some type to no longer live across a suspend point, making the generator implement an auto trait when it previously did not. While this could be surprising, I don't think it could cause a soundness issue. Whatever change the optimization pass makes shouldn't be observable by user code, so a correct optimization pass shouldn't be able to do anything that affects code using a variable across a suspend point.

Did you have something else in mind?

@varkor
Copy link
Member

varkor commented Oct 25, 2019

r? @Zoxc

@rust-highfive rust-highfive assigned Zoxc and unassigned varkor Oct 25, 2019
@matthewjasper
Copy link
Contributor

But StateTransform (and all of the passes before it) are always run. So, this can't cause the Send/Sync-ness of a generator to depend on the overall program optimization level.

The behaviour of the NoLandingPads pass depends on -C panic, which could change the captured variables.

@bors
Copy link
Contributor

bors commented Oct 26, 2019

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

@Aaron1011
Copy link
Member Author

@matthewjasper:

The behaviour of the NoLandingPads pass depends on -C panic, which could change the captured variables.

The only way for this to happen would be:

  1. We have a local containing some type with drop glue.
  2. We generate a cleanup block which drops that local (thus using it across a suspend point).
  3. There are no other uses of that local after the use of the cleanup block (e.g. TerminatorKind::Call terminator). This means that user code does not access the local, and we did not generate a normal drop for that local.

This would mean that there's a bug in the MIR generation - we're leaking a local without an explicit mem::forget (if there was a mem::forget call, that would be an access by user-generated code). This would already be a soundness issue, since some unsafe code relies on locals within its stack frame getting dropped, in order to reset the type to a valid state during a panic.

It might make sense to add a debug check where we run the generator liveness analysis both before and after the removal of unwind blocks, just to ensure that this never happens.

@Zoxc
Copy link
Contributor

Zoxc commented Oct 31, 2019

Are you concerned that these optimization passes will have some effect on the calculation of GeneratorLayout by StateTransform, which will in turn affect the auto-trait impls of the generator?

MIR optimization passes are supposed to be able to optimize away locals, and we want this to happen since it reduces the memory usage of generators. I also want MIR optimizations that will inline generators into other generators giving nice optimal state machines.

I suggest you make a function that calculates the locals that is live across a generator. Basically just copy StateTransform and rip out anything that modifies MIR. Then we can run that function before any optimizations. We'll probably want to carefully tweak / modify the semantics of that function. It will probably require a RFC too since it affects async / await. Currently the calculation in StateTransform is whatever works and we probably want something cleaner if we're lifting that into the language. Also if you added an unstable command line flag to use the new behavior, the need for a RFC would be delayed since it would no along affect stable async / await.

To break this cycle, we defer processing of all auto-trait predicates
involving ty::GeneratorWitness. These predicates are recorded in the
TypeckTables for the parent function. During MIR type-checking of the
parent function, we actually attempt to fulfill these predicates,
reporting any errors that occur.

This seem a bit sketchy to me. I'd break this cycle by changing the representation of generator types to not include it's witness or content, so you can refer to it without computing the witness or having type variables. There might be some problems with this approach that I don't see, but I'd like to keep all type checking in typeck.

I had imagined something like where we construct MIR for the generator with type variables in it, only requiring just the types needed to correctly construct MIR. Starting out with something that requires all the types in the generator is easier though and it is why I required all types in the generator to be known in order to calculate generator interior.

@Aaron1011
Copy link
Member Author

Aaron1011 commented Nov 3, 2019

This seem a bit sketchy to me. I'd break this cycle by changing the representation of generator types to not include it's witness or content, so you can refer to it without computing the witness or having type variables. There might be some problems with this approach that I don't see, but I'd like to keep all type checking in typeck.

I'm not sure what you mean. The cycles are caused by the fact that we end up trying to compute <generator>: Send during type-checking. This requires looking at the witness, regardless of how we choose to represent generators.

@Zoxc
Copy link
Contributor

Zoxc commented Nov 5, 2019

My plan was to build the MIR for the generators during type checking, so if there is a <generator>: Send bound, we make sure all the types inside the generator is known and then build the MIR for it to calculate the interior types.

@Aaron1011
Copy link
Member Author

@Zoxc: As @nikomatsakis described here, we can run into a cycle when doing this.

@JohnCSimon JohnCSimon added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Nov 9, 2019
@JohnCSimon
Copy link
Member

Ping from triage:
@Aaron1011 Can you please fix the merge conflicts here?
CC: @Zoxc @nikomatsakis

Thanks!

@Centril Centril added the F-coroutines `#![feature(coroutines)]` label Nov 10, 2019
@JohnCSimon
Copy link
Member

Pinging again from triage:
@Aaron1011 Can you please fix the merge conflicts, here?
CC: @Zoxc @nikomatsakis @Centril

Thanks!

@Aaron1011
Copy link
Member Author

I've addressed the merge conflicts.

@JohnCSimon
Copy link
Member

Thanks for addressing the merge conflicts @Aaron1011
Ping from triage:
@Zoxc is this PR ready for review?

@Zoxc
Copy link
Contributor

Zoxc commented Nov 26, 2019

@Aaron1011 Yeah, the typeck_tables cycle seem quite difficult to break. I guess this approach is the easiest way to resolve the cycle.

From a high-level view my feedback would be to copy the StateTransform analysis into a function which runs earlier (possibly right after Mir construction). Changing the interior computation to that analysis should be gated somehow so it doesn't change behavior for stable async (maybe you could change it just for generators and keep async on the old code?). You could land the analysis as is then, and refine it with future PRs before turning it on by default for async with an RFC.

I'm not currently able to review this in detail and I'm not too familiar with the trait system anyway, so I'll assign Niko for now.

r? @nikomatsakis

@rust-highfive rust-highfive assigned nikomatsakis and unassigned Zoxc Nov 26, 2019
@bors
Copy link
Contributor

bors commented Dec 8, 2019

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

@@ -117,6 +117,8 @@ pub struct Body<'tcx> {
/// to be created.
pub generator_kind: Option<GeneratorKind>,

pub generator_interior_tys: Option<Vec<Ty<'tcx>>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

These shouldn't be stored in the MIR, and should instead be the return value of the query.

/// Compute the generator interior types for a given `DefId`
/// (if it corresponds to a generator), for use in determining
/// generator auto trait implementation
query mir_generator_interior(_: DefId) -> &'tcx Steal<mir::BodyCache<'tcx>> {}
Copy link
Contributor

Choose a reason for hiding this comment

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

This won't need to modify the MIR so it should return a &'tcx List<Ty<'tcx>> (and can be renamed).

@@ -98,7 +98,12 @@ fn mir_keys(tcx: TyCtxt<'_>, krate: CrateNum) -> &DefIdSet {
}

fn mir_built(tcx: TyCtxt<'_>, def_id: DefId) -> &Steal<BodyCache<'_>> {
let mir = build::mir_build(tcx, def_id);
let mut mir = build::mir_build(tcx, def_id);
if let ty::Generator(..) = tcx.type_of(def_id).kind {
Copy link
Contributor

Choose a reason for hiding this comment

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

This still isn't quite what I meant. I've left a couple of comments explaining this.

@nikomatsakis
Copy link
Contributor

We discussed this PR on Zulip today, in this thread. In short, I'm concerned about the interactions of this sort of change with specialization. It seems like we might able to resolve it but this is a fairly "fundamental" addition that we do want to consider carefully.

@JohnCSimon JohnCSimon added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jan 18, 2020
@JohnTitor
Copy link
Member

Ping from triage: @Aaron1011 any progress on this? Should this be marked as blocked?

@nikomatsakis
Copy link
Contributor

I think it should, but we need to come up with a strategy to unblock. Not sure about that right now though.

@joelpalmer
Copy link

Triaged

@JohnTitor JohnTitor added S-blocked Status: Blocked on something else such as an RFC or other implementation work. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Mar 9, 2020
@Dylan-DPC-zz
Copy link

@Aaron1011 any updates?

@nikomatsakis
Copy link
Contributor

Hmm, I'm not sure what @Aaron1011 thinks, but I'm inclined to close this PR for the time being. I don't feel comfortable with this strategy yet, for all the reasons we talked about on Zulip.

@nikomatsakis
Copy link
Contributor

I guess I think I will feel more comfortable taking this approach once we've cleaned up our trait solver, so we are able to describe the way we are "deferring" auto-trait resolution in a more disciplined way. (I do think that, in our conversation, we discussed some vague ideas that were making sense.)

@nikomatsakis
Copy link
Contributor

I'm going to go ahead and close this PR, since I think the approach is "sufficiently blocked" that we won't be able to land it. We can always re-open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
F-coroutines `#![feature(coroutines)]` S-blocked Status: Blocked on something else such as an RFC or other implementation work.
Projects
None yet
Development

Successfully merging this pull request may close these issues.