Repository navigation
Accept variants with no fields in the variant derives - #278
Merged
Merged
Conversation
`CgpVariant`, `CgpData`, `ExtractField`, and `FromVariant` now take a
variant written `V`, `V()`, or `V {}` and give it the payload `Nil`,
matching `HasFields`. The mutable extractor builds its `&mut Nil` with
`Box::leak(Box::new(Nil))`, resolving `Box` in the caller's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CCBh7fScUH7fzq5DeNtyt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR lets the variant derives accept enum variants that have no fields.
#[derive(CgpVariant)],#[derive(CgpData)],#[derive(ExtractField)], and#[derive(FromVariant)]now take a variantwritten
Closed,Closed(), orClosed {}, and treat it as a variant whose payload isNil. AC-like enum, or an enum that mixes newtype and unit variants, can now be built, extracted, cast, and
dispatched like any other extensible variant. No input that compiled before changes its meaning: the
only inputs affected are the ones these derives used to reject.
Background: what the variant derives need
The variant derives view an enum as a sum of
Field<Tag, Payload>entries, one per variant, keyedby the variant's name. Each generated item names one payload type per variant:
FromVariant'stype Value, the partial enum's variant, and the value each extractor hands back. Until now thederives required every variant to carry exactly one unnamed field, so that field's type could serve
as the payload. A variant with no fields was rejected:
The usual workaround was a
()payload,Closed(()). That changes the enum's own API for the sakeof the derive.
#[derive(HasFields)]already accepted every variant shape, and gave a variant with no fields theempty product
Nilas its entry. The other derives therefore disagreed with the shape thatHasFieldsandEnum!describe for the same enum.What changes for the user
All three empty forms are accepted, and each carries the payload
Nil. The payload agrees with#[derive(HasFields)]andEnum!:Every extensible-variant operation then works on these variants:
Status::ClosedStatus::from_variant(PhantomData::<Symbol!("Closed")>, Nil)Status::Closedto_extractor().extract_field(...)Ok(Nil)extractor_ref().extract_field(...)Ok(&Nil)extractor_mut().extract_field(...)Ok(&mut Nil)upcastanddowncastThe dispatch matchers handle them, with one consequence to know. A value matcher such as
MatchWithValueHandlerspasses the handler only the payload. Every empty variant therefore reachesthe same
Nilhandler, and the handler cannot tell which variant it got. A field matcher such asMatchWithFieldHandlerspassesField<Symbol!("Closed"), Nil>, so its handler can read thevariant's name from the tag. The borrowed and mutable matchers work too.
Variants that still have no single payload are still rejected, with a message that now names
both accepted shapes:
Pair(u32, u32)Expected variant to contain exactly one unnamed field, or no fieldsNamed { x: u32 }Expected variant to contain exactly one unnamed field, or no fieldsHow it works
The changes are in
derive_extractor/utils.rs,with the owned, shared, and mutable extractor impls and the
FromVariantimpls updated to use it.A
VariantPayloadvalue describes each variant once.get_variant_payloadclassifies a variantas
Single(&Type)for a newtype variant orEmptyfor any of the three empty forms, and rejectsevery other shape. Each code generator asks the payload for the piece it needs: the payload type,
the match pattern, the constructor, the binding, and the value each extractor holds. A newtype
variant generates exactly what it did before.
The braced forms cover all three empty spellings. Rust lets
Self::Closed { .. }match a unit,an empty tuple, or an empty struct variant, and lets
Self::Closed {}construct any of them. So thederives emit one pattern and one constructor whatever form the user wrote. The ignored payload is
bound as
_, which keeps the generated code free of unused-variable warnings without an#[allow]attribute that would have changed every existing snapshot.
Each extractor needs a
Nilvalue, and the mutable one needs care. The owned extractor holdsNilitself, and the shared one holds&Nil, which Rust promotes to a'staticconstant. Rustdoes not promote
&mut Nil, and the variant has no field to borrow mutably, so the mutableextractor builds one with
Box::leak(Box::new(Nil)).Nilis zero-sized, soBox::newneverallocates and the leak frees nothing. Expanded for
Status:Boxis the one name the expansion takes from the caller's scope. Every other CGP name isemitted through an
exportsmarker, butcgplinks noallocand so has noBoxto export, andthis PR does not add that dependency. The derives write
Boxbare, spanned at the empty variant.A
stdcrate finds it in its prelude. Ano_stdcrate must import it, and an error without thatimport points at the variant.
The borrowed field view
FieldsRefis unchanged: an empty variant isNilthere rather than&Nil, since a borrowed product is a product of references and the empty product has none.Known issues
These limits are recorded in the knowledge base's reference documents for the derives and
#[cgp_auto_dispatch]:no_stdcrate must haveBoxin scope. Without it, the derive fails withE0433: cannot find type `Box` in this scopeat the empty variant, and rustc suggests thealloc::boxed::Boximport.#[cgp_auto_dispatch]cannot serve an enum with an empty variant. The dispatched trait wouldneed an impl for
Nil, a type fromcgp, and such an impl conflicts with the macro's blanketimpl (
E0119), as an impl for any foreign payload type such asu64does. The dispatch matcherswith a provider still work, because the crate owns the provider.