From 1e652ee1b0608ee6f0dc9a7f19d273594915b398 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 6 Oct 2026 20:27:03 +0200 Subject: [PATCH] Accept variants with no fields in the variant derives `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 Claude-Session: https://claude.ai/code/session_017CCBh7fScUH7fzq5DeNtyt --- .../derive_extractor/extract_field_impls.rs | 2 +- .../derive_extractor/has_extractor_impl.rs | 29 +- .../types/cgp_data/derive_extractor/utils.rs | 119 ++- .../src/types/cgp_data/derive_from_variant.rs | 11 +- .../parser_rejections/derive_cgp_data.rs | 8 +- .../parser_rejections/derive_from_variant.rs | 77 +- .../derive_cgp_data_empty_variants.rs | 743 ++++++++++++++++++ .../extensible_variants/empty_variants.rs | 352 +++++++++ .../tests/extensible_variants/mod.rs | 5 + 9 files changed, 1291 insertions(+), 55 deletions(-) create mode 100644 crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty_variants.rs create mode 100644 crates/tests/cgp-tests/tests/extensible_variants/empty_variants.rs diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs index 2cb348f16..cc38959aa 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs @@ -84,7 +84,7 @@ pub fn derive_extract_field_impls( if is_ref { parse_internal! { <__R__ as #MapTypeRef>::Map<'__a__, #value_type> } } else { - value_type.clone() + value_type } }; diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs index e20a9631f..6154a6b64 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs @@ -4,7 +4,7 @@ use syn::{Arm, Ident, ItemEnum, ItemImpl}; use crate::exports::{HasExtractor, HasExtractorMut, HasExtractorRef, IsMut, IsPresent, IsRef}; use crate::functions::override_item_span; use crate::parse_internal; -use crate::types::cgp_data::to_generic_args; +use crate::types::cgp_data::{get_variant_payload, to_generic_args}; /// Emit the owned `HasExtractor` impl: `to_extractor`/`from_extractor` map each /// variant to and from the all-`IsPresent` configuration of the partial enum. @@ -28,16 +28,21 @@ pub fn derive_has_extractor_impl( }); let variant_ident = &variant.ident; + let payload = get_variant_payload(variant)?; + let pattern = payload.match_pattern(variant); + let value = payload.owned_value(); + let constructor = payload.constructor(variant); + let binding = payload.binding(); to_match_arms.push(parse_internal! { - Self :: #variant_ident ( value ) => { - #extractor_ident:: #variant_ident ( value ) + #pattern => { + #extractor_ident:: #variant_ident ( #value ) } }); from_match_arms.push(parse_internal! { - #extractor_ident:: #variant_ident ( value ) => { - Self :: #variant_ident ( value ) + #extractor_ident:: #variant_ident ( #binding ) => { + #constructor } }); } @@ -111,10 +116,13 @@ pub fn derive_has_extractor_ref_impl( }); let variant_ident = &variant.ident; + let payload = get_variant_payload(variant)?; + let pattern = payload.match_pattern(variant); + let value = payload.ref_value(); match_arms.push(parse_internal! { - Self :: #variant_ident ( value ) => { - #extractor_ident:: #variant_ident ( value ) + #pattern => { + #extractor_ident:: #variant_ident ( #value ) } }); } @@ -185,10 +193,13 @@ pub fn derive_has_extractor_mut_impl( }); let variant_ident = &variant.ident; + let payload = get_variant_payload(variant)?; + let pattern = payload.match_pattern(variant); + let value = payload.mut_value(variant); match_arms.push(parse_internal! { - Self :: #variant_ident ( value ) => { - #extractor_ident:: #variant_ident ( value ) + #pattern => { + #extractor_ident:: #variant_ident ( #value ) } }); } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/utils.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/utils.rs index 65eeb25cf..483f49355 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/utils.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/utils.rs @@ -1,26 +1,133 @@ +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::{Field, FieldMutability, Fields, FieldsUnnamed, Type, Variant, Visibility}; -/// Return a newtype variant's single payload type, or a spanned error if the -/// variant is not exactly one unnamed field — the requirement the variant -/// derives enforce. -pub fn get_variant_type(variant: &Variant) -> syn::Result<&Type> { +use crate::exports::Nil; +use crate::parse_internal; + +/// The payload of a variant the variant derives accept: either the single +/// unnamed field of a newtype variant, or `Nil` for a variant with no fields. +pub enum VariantPayload<'a> { + /// A newtype variant `V(T)`, whose payload is `T`. + Single(&'a Type), + /// A variant with no fields, written `V`, `V()`, or `V {}`, whose payload is + /// `Nil`, the empty product `#[derive(HasFields)]` already gives it. + Empty, +} + +impl VariantPayload<'_> { + /// The payload type: the field's type, or `Nil`. + pub fn payload_type(&self) -> syn::Result { + match self { + Self::Single(field_type) => Ok((*field_type).clone()), + Self::Empty => Ok(parse_internal! { #Nil }), + } + } + + /// A pattern matching the variant on the original enum, binding the payload + /// to `value`. An empty variant is matched by `{ .. }`, which matches all + /// three of its forms. + pub fn match_pattern(&self, variant: &Variant) -> TokenStream { + let variant_ident = &variant.ident; + + match self { + Self::Single(_) => quote! { Self :: #variant_ident ( value ) }, + Self::Empty => quote! { Self :: #variant_ident { .. } }, + } + } + + /// An expression building the variant on the original enum from a payload + /// bound to `value`. An empty variant is built by `{}`, which builds all + /// three of its forms and ignores the payload. + pub fn constructor(&self, variant: &Variant) -> TokenStream { + let variant_ident = &variant.ident; + + match self { + Self::Single(_) => quote! { Self :: #variant_ident ( value ) }, + Self::Empty => quote! { Self :: #variant_ident {} }, + } + } + + /// The binding for a payload the variant is built from: `value` for a newtype + /// variant, and `_` for an empty one, which ignores its `Nil`. + pub fn binding(&self) -> TokenStream { + match self { + Self::Single(_) => quote! { value }, + Self::Empty => quote! { _ }, + } + } + + /// The payload the owned extractor holds for a matched variant. + pub fn owned_value(&self) -> TokenStream { + match self { + Self::Single(_) => quote! { value }, + Self::Empty => quote! { #Nil }, + } + } + + /// The payload the shared borrowed extractor holds: a promoted `&Nil` for an + /// empty variant, since it has no field to borrow. + pub fn ref_value(&self) -> TokenStream { + match self { + Self::Single(_) => quote! { value }, + Self::Empty => quote! { &#Nil }, + } + } + + /// The payload the mutable borrowed extractor holds. An empty variant has no + /// field to borrow and Rust does not promote `&mut Nil`, so the payload is a + /// leaked `Box`, which never allocates because `Nil` is zero-sized. + /// + /// `Box` is written bare and resolved where the derive is used, the one name + /// a CGP expansion takes from the caller's scope rather than from `cgp`, + /// which links no `alloc`. It is spanned at the variant, so a `no_std` crate + /// without `Box` in scope sees the error on the empty variant. + pub fn mut_value(&self, variant: &Variant) -> TokenStream { + match self { + Self::Single(_) => quote! { value }, + Self::Empty => { + let span = variant.ident.span(); + let boxed = quote_spanned! { span => Box }; + + quote! { #boxed::leak(#boxed::new(#Nil)) } + } + } + } +} + +/// Return a variant's payload, or a spanned error if the variant has several +/// fields or named fields, which the variant derives cannot carry as one payload. +pub fn get_variant_payload(variant: &Variant) -> syn::Result> { match &variant.fields { Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { if let Some(field) = fields.unnamed.first() { - return Ok(&field.ty); + return Ok(VariantPayload::Single(&field.ty)); } } + Fields::Unnamed(fields) if fields.unnamed.is_empty() => { + return Ok(VariantPayload::Empty); + } + Fields::Named(fields) if fields.named.is_empty() => { + return Ok(VariantPayload::Empty); + } + Fields::Unit => return Ok(VariantPayload::Empty), _ => {} } Err(syn::Error::new( variant.span(), - "Expected variant to contain exactly one unnamed field", + "Expected variant to contain exactly one unnamed field, or no fields", )) } +/// Return a variant's payload type: the newtype field's type, or `Nil` for a +/// variant with no fields. +pub fn get_variant_type(variant: &Variant) -> syn::Result { + get_variant_payload(variant)?.payload_type() +} + /// Wrap a type as a single-unnamed-field variant body, used to re-shape a /// partial enum's variants around their `MapType`-wrapped payloads. pub fn type_to_variant_fields(type_: &Type) -> Fields { diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs index b6a52584c..1f3c4ef5c 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs @@ -3,7 +3,7 @@ use syn::{ItemEnum, ItemImpl}; use crate::exports::FromVariant; use crate::functions::override_item_span; use crate::parse_internal; -use crate::types::cgp_data::get_variant_type; +use crate::types::cgp_data::get_variant_payload; use crate::types::field::Symbol; /// Emit one `FromVariant` impl per variant, each keyed by the variant name's @@ -19,7 +19,10 @@ pub fn derive_from_variant_from_enum(item_enum: &ItemEnum) -> syn::Result for #enum_ident #ty_generics @@ -27,8 +30,8 @@ pub fn derive_from_variant_from_enum(item_enum: &ItemEnum) -> syn::Result, value: Self::Value) -> Self { - Self::#variant_ident(value) + fn from_variant(_tag: ::core::marker::PhantomData<#variant_tag>, #binding: Self::Value) -> Self { + #constructor } } }; diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/derive_cgp_data.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/derive_cgp_data.rs index 385d431c5..0ec0e1b39 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/derive_cgp_data.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/derive_cgp_data.rs @@ -1,7 +1,7 @@ //! `#[derive(CgpData)]` — the umbrella derive — rejects the inputs its shape //! dispatch and variant codegen cannot lower: an item that is neither a struct -//! nor an enum, and (on the enum path) a variant that is not a single unnamed -//! field. The bad-variant rejection is inherited from the shared variant codegen +//! nor an enum, and (on the enum path) a variant with several fields or with +//! named fields. The bad-variant rejection is inherited from the shared variant codegen //! that `#[derive(CgpVariant)]`/`#[derive(FromVariant)]` also use, so this only //! confirms the umbrella propagates it; the non-struct-or-enum rejection is //! unique to `ItemCgpData`'s parser. @@ -28,8 +28,8 @@ fn rejects_non_struct_or_enum() { #[test] fn rejects_struct_style_variant_on_enum() { - // On the enum path the umbrella runs the same single-unnamed-field variant - // codegen as `#[derive(CgpVariant)]`, so a struct-style variant is refused. + // On the enum path the umbrella runs the same variant codegen as + // `#[derive(CgpVariant)]`, so a struct-style variant with fields is refused. assert_macro_rejects("derive(CgpData) on a struct-style variant", || { cgp_macro_lib::derive_cgp_data(quote!( pub enum Shape { diff --git a/crates/tests/cgp-macro-tests/tests/parser_rejections/derive_from_variant.rs b/crates/tests/cgp-macro-tests/tests/parser_rejections/derive_from_variant.rs index 06fcb3cce..cf66a270e 100644 --- a/crates/tests/cgp-macro-tests/tests/parser_rejections/derive_from_variant.rs +++ b/crates/tests/cgp-macro-tests/tests/parser_rejections/derive_from_variant.rs @@ -1,6 +1,7 @@ //! `#[derive(FromVariant)]` and `#[derive(CgpVariant)]` reject the enum shapes -//! their codegen cannot lower: a variant that does not carry exactly one unnamed -//! field, and (for `CgpVariant`) a non-enum item. +//! their codegen cannot lower: a variant with several fields or with named +//! fields, which has no single payload, and (for `CgpVariant`) a non-enum item. +//! A variant with no fields is accepted, with the payload `Nil`. //! //! See cgp-knowledge-base/cgp/implementation/entrypoints/derive_from_variant.md (Known issues, //! Tests) and cgp-knowledge-base/cgp/implementation/entrypoints/derive_cgp_variant.md for the @@ -8,46 +9,60 @@ use quote::quote; -use super::assert_macro_rejects; +use super::{assert_macro_rejects, assert_macro_rejects_with}; -#[test] -fn rejects_fieldless_variant() { - // A unit variant carries no payload, so there is no value type for the - // constructor to wrap. - assert_macro_rejects("derive(FromVariant) on a fieldless variant", || { - cgp_macro_lib::derive_from_variant(quote!( - pub enum Shape { - Circle(Circle), - Empty, - } - )) - }); -} +const VARIANT_SHAPE_MESSAGE: &str = + "Expected variant to contain exactly one unnamed field, or no fields"; #[test] fn rejects_multi_field_variant() { // A multi-field tuple variant has more than one payload; the constructor // keys a single `Value` per variant, so the shape is refused. - assert_macro_rejects("derive(FromVariant) on a multi-field variant", || { - cgp_macro_lib::derive_from_variant(quote!( - pub enum Shape { - Pair(u32, u32), - } - )) - }); + assert_macro_rejects_with( + "derive(FromVariant) on a multi-field variant", + VARIANT_SHAPE_MESSAGE, + || { + cgp_macro_lib::derive_from_variant(quote!( + pub enum Shape { + Pair(u32, u32), + } + )) + }, + ); } #[test] fn rejects_struct_style_variant() { // A struct-style variant names its fields; the constructor expects a single - // unnamed field. - assert_macro_rejects("derive(FromVariant) on a struct-style variant", || { - cgp_macro_lib::derive_from_variant(quote!( - pub enum Shape { - Named { x: u32 }, - } - )) - }); + // unnamed field. An empty struct-style variant, `Named {}`, is accepted. + assert_macro_rejects_with( + "derive(FromVariant) on a struct-style variant", + VARIANT_SHAPE_MESSAGE, + || { + cgp_macro_lib::derive_from_variant(quote!( + pub enum Shape { + Named { x: u32 }, + } + )) + }, + ); +} + +#[test] +fn extract_field_rejects_struct_style_variant() { + // The extractor derive shares the constructor's variant-shape check. + assert_macro_rejects_with( + "derive(ExtractField) on a struct-style variant", + VARIANT_SHAPE_MESSAGE, + || { + cgp_macro_lib::derive_extract_field(quote!( + pub enum Shape { + Circle(Circle), + Named { x: u32 }, + } + )) + }, + ); } #[test] diff --git a/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty_variants.rs b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty_variants.rs new file mode 100644 index 000000000..0783cec8d --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_variants/derive_cgp_data_empty_variants.rs @@ -0,0 +1,743 @@ +//! `#[derive(CgpData)]` on an enum mixing a newtype variant with the three forms +//! of a variant with no fields: `Closed`, `Paused()`, and `Archived {}`. Each +//! empty variant carries the payload `Nil` throughout: `HasFields` lists it as +//! `Field`, `FromVariant` takes a `Nil` and builds the variant with +//! `{}`, and the extractors match it with `{ .. }`, both braced forms working for +//! all three. The owned extractor holds `Nil`, the shared one a promoted `&Nil`, +//! and the mutable one a leaked `Box`, since Rust does not promote +//! `&mut Nil`. That `Box` is written bare and resolved where the derive is used. +//! +//! This concept owns the variant expansion of `#[derive(CgpData)]`; this file is +//! the empty-variant snapshot, and `empty_variants.rs` exercises its behavior. +//! +//! See cgp-knowledge-base/cgp/reference/derives/derive_cgp_variant.md and +//! cgp-knowledge-base/cgp/implementation/entrypoints/derive_extract_field.md. + +use cgp_macro_test_util::snapshot_derive_cgp_data; + +snapshot_derive_cgp_data! { + #[derive(CgpData)] + pub enum Status { + Active(u64), + Closed, + Paused(), + Archived {}, + } + + expand_status(output) { + insta::assert_snapshot!(output, @" + impl HasFields for Status { + type Fields = Either< + Field< + Symbol< + 6, + Chars< + 'A', + Chars<'c', Chars<'t', Chars<'i', Chars<'v', Chars<'e', Nil>>>>>, + >, + >, + u64, + >, + Either< + Field< + Symbol< + 6, + Chars< + 'C', + Chars<'l', Chars<'o', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + Nil, + >, + Either< + Field< + Symbol< + 6, + Chars< + 'P', + Chars< + 'a', + Chars<'u', Chars<'s', Chars<'e', Chars<'d', Nil>>>>, + >, + >, + >, + Nil, + >, + Either< + Field< + Symbol< + 8, + Chars< + 'A', + Chars< + 'r', + Chars< + 'c', + Chars< + 'h', + Chars<'i', Chars<'v', Chars<'e', Chars<'d', Nil>>>>, + >, + >, + >, + >, + >, + Nil, + >, + Void, + >, + >, + >, + >; + } + impl HasFieldsRef for Status { + type FieldsRef<'__a> = Either< + Field< + Symbol< + 6, + Chars< + 'A', + Chars<'c', Chars<'t', Chars<'i', Chars<'v', Chars<'e', Nil>>>>>, + >, + >, + &'__a u64, + >, + Either< + Field< + Symbol< + 6, + Chars< + 'C', + Chars<'l', Chars<'o', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + Nil, + >, + Either< + Field< + Symbol< + 6, + Chars< + 'P', + Chars< + 'a', + Chars<'u', Chars<'s', Chars<'e', Chars<'d', Nil>>>>, + >, + >, + >, + Nil, + >, + Either< + Field< + Symbol< + 8, + Chars< + 'A', + Chars< + 'r', + Chars< + 'c', + Chars< + 'h', + Chars<'i', Chars<'v', Chars<'e', Chars<'d', Nil>>>>, + >, + >, + >, + >, + >, + Nil, + >, + Void, + >, + >, + >, + > + where + Self: '__a; + } + impl FromFields for Status { + fn from_fields(rest: Self::Fields) -> Self { + match rest { + Either::Left(field) => { + let field = field.value; + Self::Active(field) + } + Either::Right(rest) => { + match rest { + Either::Left(field) => { + let Nil = field.value; + Self::Closed + } + Either::Right(rest) => { + match rest { + Either::Left(field) => { + let Nil = field.value; + Self::Paused() + } + Either::Right(rest) => { + match rest { + Either::Left(field) => { + let Nil = field.value; + Self::Archived {} + } + Either::Right(rest) => match rest {} + } + } + } + } + } + } + } + } + } + impl ToFields for Status { + fn to_fields(self) -> Self::Fields { + match self { + Self::Active(field) => Either::Left(field.into()), + Self::Closed => Either::Right(Either::Left(Nil.into())), + Self::Paused() => Either::Right(Either::Right(Either::Left(Nil.into()))), + Self::Archived {} => { + Either::Right(Either::Right(Either::Right(Either::Left(Nil.into())))) + } + } + } + } + impl ToFieldsRef for Status { + fn to_fields_ref<'__a>(&'__a self) -> Self::FieldsRef<'__a> + where + Self: '__a, + { + match self { + Self::Active(field) => Either::Left(field.into()), + Self::Closed => Either::Right(Either::Left(Nil.into())), + Self::Paused() => Either::Right(Either::Right(Either::Left(Nil.into()))), + Self::Archived {} => { + Either::Right(Either::Right(Either::Right(Either::Left(Nil.into())))) + } + } + } + } + impl FromVariant< + Symbol< + 6, + Chars<'A', Chars<'c', Chars<'t', Chars<'i', Chars<'v', Chars<'e', Nil>>>>>>, + >, + > for Status { + type Value = u64; + fn from_variant( + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'A', + Chars<'c', Chars<'t', Chars<'i', Chars<'v', Chars<'e', Nil>>>>>, + >, + >, + >, + value: Self::Value, + ) -> Self { + Self::Active(value) + } + } + impl FromVariant< + Symbol< + 6, + Chars<'C', Chars<'l', Chars<'o', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>>, + >, + > for Status { + type Value = Nil; + fn from_variant( + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'C', + Chars<'l', Chars<'o', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + >, + _: Self::Value, + ) -> Self { + Self::Closed {} + } + } + impl FromVariant< + Symbol< + 6, + Chars<'P', Chars<'a', Chars<'u', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>>, + >, + > for Status { + type Value = Nil; + fn from_variant( + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'P', + Chars<'a', Chars<'u', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + >, + _: Self::Value, + ) -> Self { + Self::Paused {} + } + } + impl FromVariant< + Symbol< + 8, + Chars< + 'A', + Chars< + 'r', + Chars< + 'c', + Chars<'h', Chars<'i', Chars<'v', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + >, + >, + > for Status { + type Value = Nil; + fn from_variant( + _tag: ::core::marker::PhantomData< + Symbol< + 8, + Chars< + 'A', + Chars< + 'r', + Chars< + 'c', + Chars< + 'h', + Chars<'i', Chars<'v', Chars<'e', Chars<'d', Nil>>>>, + >, + >, + >, + >, + >, + >, + _: Self::Value, + ) -> Self { + Self::Archived {} + } + } + pub enum __PartialStatus< + __F0__: MapType, + __F1__: MapType, + __F2__: MapType, + __F3__: MapType, + > { + Active(<__F0__ as MapType>::Map), + Closed(<__F1__ as MapType>::Map), + Paused(<__F2__ as MapType>::Map), + Archived(<__F3__ as MapType>::Map), + } + pub enum __PartialRefStatus< + '__a__, + __R__: MapTypeRef, + __F0__: MapType, + __F1__: MapType, + __F2__: MapType, + __F3__: MapType, + > { + Active(<__F0__ as MapType>::Map<<__R__ as MapTypeRef>::Map<'__a__, u64>>), + Closed(<__F1__ as MapType>::Map<<__R__ as MapTypeRef>::Map<'__a__, Nil>>), + Paused(<__F2__ as MapType>::Map<<__R__ as MapTypeRef>::Map<'__a__, Nil>>), + Archived(<__F3__ as MapType>::Map<<__R__ as MapTypeRef>::Map<'__a__, Nil>>), + } + impl<__F0__: MapType, __F1__: MapType, __F2__: MapType, __F3__: MapType> PartialData + for __PartialStatus<__F0__, __F1__, __F2__, __F3__> { + type Target = Status; + } + impl< + '__a__, + __R__: MapTypeRef, + __F0__: MapType, + __F1__: MapType, + __F2__: MapType, + __F3__: MapType, + > PartialData for __PartialRefStatus<'__a__, __R__, __F0__, __F1__, __F2__, __F3__> { + type Target = Status; + } + impl HasExtractor for Status { + type Extractor = __PartialStatus; + fn to_extractor(self) -> Self::Extractor { + match self { + Self::Active(value) => __PartialStatus::Active(value), + Self::Closed { .. } => __PartialStatus::Closed(Nil), + Self::Paused { .. } => __PartialStatus::Paused(Nil), + Self::Archived { .. } => __PartialStatus::Archived(Nil), + } + } + fn from_extractor(extractor: Self::Extractor) -> Self { + match extractor { + __PartialStatus::Active(value) => Self::Active(value), + __PartialStatus::Closed(_) => Self::Closed {}, + __PartialStatus::Paused(_) => Self::Paused {}, + __PartialStatus::Archived(_) => Self::Archived {}, + } + } + } + impl HasExtractorRef for Status { + type ExtractorRef<'__a__> = __PartialRefStatus< + '__a__, + IsRef, + IsPresent, + IsPresent, + IsPresent, + IsPresent, + > + where + Self: '__a__; + fn extractor_ref<'__a__>(&'__a__ self) -> Self::ExtractorRef<'__a__> { + match self { + Self::Active(value) => __PartialRefStatus::Active(value), + Self::Closed { .. } => __PartialRefStatus::Closed(&Nil), + Self::Paused { .. } => __PartialRefStatus::Paused(&Nil), + Self::Archived { .. } => __PartialRefStatus::Archived(&Nil), + } + } + } + impl HasExtractorMut for Status { + type ExtractorMut<'__a__> = __PartialRefStatus< + '__a__, + IsMut, + IsPresent, + IsPresent, + IsPresent, + IsPresent, + > + where + Self: '__a__; + fn extractor_mut<'__a__>(&'__a__ mut self) -> Self::ExtractorMut<'__a__> { + match self { + Self::Active(value) => __PartialRefStatus::Active(value), + Self::Closed { .. } => __PartialRefStatus::Closed(Box::leak(Box::new(Nil))), + Self::Paused { .. } => __PartialRefStatus::Paused(Box::leak(Box::new(Nil))), + Self::Archived { .. } => { + __PartialRefStatus::Archived(Box::leak(Box::new(Nil))) + } + } + } + } + impl FinalizeExtract for __PartialStatus { + fn finalize_extract<__T__>(self) -> __T__ { + match self {} + } + } + impl<'__a__, __R__: MapTypeRef> FinalizeExtract + for __PartialRefStatus<'__a__, __R__, IsVoid, IsVoid, IsVoid, IsVoid> { + fn finalize_extract<__T__>(self) -> __T__ { + match self {} + } + } + impl< + __F1__: MapType, + __F2__: MapType, + __F3__: MapType, + > ExtractField< + Symbol< + 6, + Chars<'A', Chars<'c', Chars<'t', Chars<'i', Chars<'v', Chars<'e', Nil>>>>>>, + >, + > for __PartialStatus { + type Value = u64; + type Remainder = __PartialStatus; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'A', + Chars<'c', Chars<'t', Chars<'i', Chars<'v', Chars<'e', Nil>>>>>, + >, + >, + >, + ) -> Result { + match self { + __PartialStatus::Active(value) => Ok(value), + __PartialStatus::Closed(value) => Err(__PartialStatus::Closed(value)), + __PartialStatus::Paused(value) => Err(__PartialStatus::Paused(value)), + __PartialStatus::Archived(value) => Err(__PartialStatus::Archived(value)), + } + } + } + impl< + __F0__: MapType, + __F2__: MapType, + __F3__: MapType, + > ExtractField< + Symbol< + 6, + Chars<'C', Chars<'l', Chars<'o', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>>, + >, + > for __PartialStatus<__F0__, IsPresent, __F2__, __F3__> { + type Value = Nil; + type Remainder = __PartialStatus<__F0__, IsVoid, __F2__, __F3__>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'C', + Chars<'l', Chars<'o', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + >, + ) -> Result { + match self { + __PartialStatus::Active(value) => Err(__PartialStatus::Active(value)), + __PartialStatus::Closed(value) => Ok(value), + __PartialStatus::Paused(value) => Err(__PartialStatus::Paused(value)), + __PartialStatus::Archived(value) => Err(__PartialStatus::Archived(value)), + } + } + } + impl< + __F0__: MapType, + __F1__: MapType, + __F3__: MapType, + > ExtractField< + Symbol< + 6, + Chars<'P', Chars<'a', Chars<'u', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>>, + >, + > for __PartialStatus<__F0__, __F1__, IsPresent, __F3__> { + type Value = Nil; + type Remainder = __PartialStatus<__F0__, __F1__, IsVoid, __F3__>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'P', + Chars<'a', Chars<'u', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + >, + ) -> Result { + match self { + __PartialStatus::Active(value) => Err(__PartialStatus::Active(value)), + __PartialStatus::Closed(value) => Err(__PartialStatus::Closed(value)), + __PartialStatus::Paused(value) => Ok(value), + __PartialStatus::Archived(value) => Err(__PartialStatus::Archived(value)), + } + } + } + impl< + __F0__: MapType, + __F1__: MapType, + __F2__: MapType, + > ExtractField< + Symbol< + 8, + Chars< + 'A', + Chars< + 'r', + Chars< + 'c', + Chars<'h', Chars<'i', Chars<'v', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + >, + >, + > for __PartialStatus<__F0__, __F1__, __F2__, IsPresent> { + type Value = Nil; + type Remainder = __PartialStatus<__F0__, __F1__, __F2__, IsVoid>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 8, + Chars< + 'A', + Chars< + 'r', + Chars< + 'c', + Chars< + 'h', + Chars<'i', Chars<'v', Chars<'e', Chars<'d', Nil>>>>, + >, + >, + >, + >, + >, + >, + ) -> Result { + match self { + __PartialStatus::Active(value) => Err(__PartialStatus::Active(value)), + __PartialStatus::Closed(value) => Err(__PartialStatus::Closed(value)), + __PartialStatus::Paused(value) => Err(__PartialStatus::Paused(value)), + __PartialStatus::Archived(value) => Ok(value), + } + } + } + impl< + '__a__, + __R__: MapTypeRef, + __F1__: MapType, + __F2__: MapType, + __F3__: MapType, + > ExtractField< + Symbol< + 6, + Chars<'A', Chars<'c', Chars<'t', Chars<'i', Chars<'v', Chars<'e', Nil>>>>>>, + >, + > for __PartialRefStatus<'__a__, __R__, IsPresent, __F1__, __F2__, __F3__> { + type Value = <__R__ as MapTypeRef>::Map<'__a__, u64>; + type Remainder = __PartialRefStatus<'__a__, __R__, IsVoid, __F1__, __F2__, __F3__>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'A', + Chars<'c', Chars<'t', Chars<'i', Chars<'v', Chars<'e', Nil>>>>>, + >, + >, + >, + ) -> Result { + match self { + __PartialRefStatus::Active(value) => Ok(value), + __PartialRefStatus::Closed(value) => Err(__PartialRefStatus::Closed(value)), + __PartialRefStatus::Paused(value) => Err(__PartialRefStatus::Paused(value)), + __PartialRefStatus::Archived(value) => { + Err(__PartialRefStatus::Archived(value)) + } + } + } + } + impl< + '__a__, + __R__: MapTypeRef, + __F0__: MapType, + __F2__: MapType, + __F3__: MapType, + > ExtractField< + Symbol< + 6, + Chars<'C', Chars<'l', Chars<'o', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>>, + >, + > for __PartialRefStatus<'__a__, __R__, __F0__, IsPresent, __F2__, __F3__> { + type Value = <__R__ as MapTypeRef>::Map<'__a__, Nil>; + type Remainder = __PartialRefStatus<'__a__, __R__, __F0__, IsVoid, __F2__, __F3__>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'C', + Chars<'l', Chars<'o', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + >, + ) -> Result { + match self { + __PartialRefStatus::Active(value) => Err(__PartialRefStatus::Active(value)), + __PartialRefStatus::Closed(value) => Ok(value), + __PartialRefStatus::Paused(value) => Err(__PartialRefStatus::Paused(value)), + __PartialRefStatus::Archived(value) => { + Err(__PartialRefStatus::Archived(value)) + } + } + } + } + impl< + '__a__, + __R__: MapTypeRef, + __F0__: MapType, + __F1__: MapType, + __F3__: MapType, + > ExtractField< + Symbol< + 6, + Chars<'P', Chars<'a', Chars<'u', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>>, + >, + > for __PartialRefStatus<'__a__, __R__, __F0__, __F1__, IsPresent, __F3__> { + type Value = <__R__ as MapTypeRef>::Map<'__a__, Nil>; + type Remainder = __PartialRefStatus<'__a__, __R__, __F0__, __F1__, IsVoid, __F3__>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 6, + Chars< + 'P', + Chars<'a', Chars<'u', Chars<'s', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + >, + ) -> Result { + match self { + __PartialRefStatus::Active(value) => Err(__PartialRefStatus::Active(value)), + __PartialRefStatus::Closed(value) => Err(__PartialRefStatus::Closed(value)), + __PartialRefStatus::Paused(value) => Ok(value), + __PartialRefStatus::Archived(value) => { + Err(__PartialRefStatus::Archived(value)) + } + } + } + } + impl< + '__a__, + __R__: MapTypeRef, + __F0__: MapType, + __F1__: MapType, + __F2__: MapType, + > ExtractField< + Symbol< + 8, + Chars< + 'A', + Chars< + 'r', + Chars< + 'c', + Chars<'h', Chars<'i', Chars<'v', Chars<'e', Chars<'d', Nil>>>>>, + >, + >, + >, + >, + > for __PartialRefStatus<'__a__, __R__, __F0__, __F1__, __F2__, IsPresent> { + type Value = <__R__ as MapTypeRef>::Map<'__a__, Nil>; + type Remainder = __PartialRefStatus<'__a__, __R__, __F0__, __F1__, __F2__, IsVoid>; + fn extract_field( + self, + _tag: ::core::marker::PhantomData< + Symbol< + 8, + Chars< + 'A', + Chars< + 'r', + Chars< + 'c', + Chars< + 'h', + Chars<'i', Chars<'v', Chars<'e', Chars<'d', Nil>>>>, + >, + >, + >, + >, + >, + >, + ) -> Result { + match self { + __PartialRefStatus::Active(value) => Err(__PartialRefStatus::Active(value)), + __PartialRefStatus::Closed(value) => Err(__PartialRefStatus::Closed(value)), + __PartialRefStatus::Paused(value) => Err(__PartialRefStatus::Paused(value)), + __PartialRefStatus::Archived(value) => Ok(value), + } + } + } + ") + } +} diff --git a/crates/tests/cgp-tests/tests/extensible_variants/empty_variants.rs b/crates/tests/cgp-tests/tests/extensible_variants/empty_variants.rs new file mode 100644 index 000000000..b24f858fb --- /dev/null +++ b/crates/tests/cgp-tests/tests/extensible_variants/empty_variants.rs @@ -0,0 +1,352 @@ +//! Variants with no fields, in each of their three forms (`Closed`, `Paused()`, +//! and `Archived {}`), under the variant derives. Each carries the payload `Nil`, +//! the empty product `#[derive(HasFields)]` gives it, so it is built, extracted, +//! cast, and dispatched like any newtype variant. +//! +//! The borrowed extractors hold a reference to `Nil`: a promoted `&Nil` when +//! shared, and a leaked `Box` when mutable, since `&mut Nil` is not +//! promoted. That keeps every borrowed matcher working, the mutable ones +//! included. `#[cgp_auto_dispatch]` cannot serve such an enum, because a crate +//! implementing the dispatched trait for the foreign `Nil` conflicts with the +//! blanket impl the macro emits, so the value dispatch here uses providers. +//! +//! See cgp-knowledge-base/cgp/reference/derives/derive_cgp_variant.md and +//! cgp-knowledge-base/cgp/implementation/entrypoints/derive_extract_field.md. + +use core::convert::Infallible; +use core::marker::PhantomData; + +use cgp::core::error::ErrorTypeProviderComponent; +use cgp::core::field::impls::{CanDowncast, CanUpcast}; +use cgp::core::field::traits::{FinalizeExtractResult, StaticString}; +use cgp::extra::dispatch::{ + MatchWithFieldHandlers, MatchWithValueHandlers, MatchWithValueHandlersMut, + MatchWithValueHandlersRef, +}; +use cgp::extra::handler::Computer; +use cgp::prelude::*; + +#[derive(Debug, PartialEq, CgpVariant)] +pub enum Status { + Active(Count), + Closed, + Paused(), + Archived {}, +} + +#[derive(Debug, PartialEq, CgpVariant)] +pub enum OpenStatus { + Active(Count), + Paused(), +} + +#[derive(Debug, PartialEq)] +pub struct Count(pub u64); + +/// A C-like enum: every variant is empty. +#[derive(Debug, PartialEq, CgpVariant)] +pub enum Light { + Red, + Amber, + Green, +} + +/// The shape `#[derive(HasFields)]` gives `Status`, written out. +type StatusFields = Sum![ + Field, + Field, + Field, + Field, +]; + +/// The same shape, written as the enum body `Enum!` reads. +type StatusShape = Enum! { + Active(Count), + Closed, + Paused(), + Archived {}, +}; + +fn assert_same_type(_: PhantomData, _: PhantomData) {} + +#[test] +fn test_has_fields_gives_nil_for_every_empty_form() { + assert_same_type( + PhantomData::<::Fields>, + PhantomData::, + ); + assert_same_type(PhantomData::, PhantomData::); +} + +#[test] +fn test_from_variant_builds_each_empty_form() { + assert_eq!( + Status::from_variant(PhantomData::, Nil), + Status::Closed + ); + assert_eq!( + Status::from_variant(PhantomData::, Nil), + Status::Paused() + ); + assert_eq!( + Status::from_variant(PhantomData::, Nil), + Status::Archived {} + ); + assert_eq!( + Status::from_variant(PhantomData::, Count(3)), + Status::Active(Count(3)) + ); +} + +#[test] +fn test_extract_empty_variants_owned() { + let extracted = Status::Archived {} + .to_extractor() + .extract_field(PhantomData::); + assert_eq!(extracted.ok(), Some(Nil)); + + let remainder = Status::Closed + .to_extractor() + .extract_field(PhantomData::) + .unwrap_err(); + assert_eq!( + remainder + .extract_field(PhantomData::) + .ok(), + Some(Nil) + ); + + assert_eq!( + Status::from_extractor(Status::Paused().to_extractor()), + Status::Paused() + ); +} + +#[test] +fn test_extract_empty_variants_borrowed() { + let status = Status::Paused(); + let shared: Result<&Nil, _> = status + .extractor_ref() + .extract_field(PhantomData::); + assert_eq!(shared.ok(), Some(&Nil)); + + let mut status = Status::Closed; + let unique: Result<&mut Nil, _> = status + .extractor_mut() + .extract_field(PhantomData::); + assert_eq!(unique.ok(), Some(&mut Nil)); +} + +/// Name the variant a value is, by extracting every variant in turn and +/// finalizing the empty remainder, which proves the match exhaustive. +fn light_name(light: Light) -> &'static str { + let remainder = match light + .to_extractor() + .extract_field(PhantomData::) + { + Ok(Nil) => return "red", + Err(remainder) => remainder, + }; + + let remainder = match remainder.extract_field(PhantomData::) { + Ok(Nil) => return "amber", + Err(remainder) => remainder, + }; + + let Nil = remainder + .extract_field(PhantomData::) + .finalize_extract_result(); + + "green" +} + +#[test] +fn test_c_like_enum() { + assert_eq!(light_name(Light::Red), "red"); + assert_eq!(light_name(Light::Amber), "amber"); + assert_eq!(light_name(Light::Green), "green"); + + let light = Light::Amber; + assert!( + light + .extractor_ref() + .extract_field(PhantomData::) + .is_ok() + ); +} + +#[test] +fn test_cast_through_empty_variants() { + assert_eq!( + OpenStatus::Paused().upcast(PhantomData::), + Status::Paused() + ); + + assert_eq!( + Status::Paused().downcast(PhantomData::).ok(), + Some(OpenStatus::Paused()) + ); + + assert_eq!( + Status::Closed.downcast(PhantomData::).ok(), + None + ); +} + +/// An empty variant beside a generic payload and a borrowed one, so the partial enums carry both a +/// type parameter and the enum's own lifetime alongside the `Nil` payload. +#[derive(Debug, PartialEq, CgpVariant)] +pub enum Lookup<'a, T> { + Found(T), + Borrowed(&'a str), + Missing, +} + +#[test] +fn test_empty_variant_beside_generics_and_lifetimes() { + assert_eq!( + Lookup::::from_variant(PhantomData::, Nil), + Lookup::Missing + ); + + let lookup: Lookup<'_, u64> = Lookup::Missing; + let shared: Result<&Nil, _> = lookup + .extractor_ref() + .extract_field(PhantomData::); + assert_eq!(shared.ok(), Some(&Nil)); + + let mut lookup: Lookup<'_, u64> = Lookup::Missing; + let unique: Result<&mut Nil, _> = lookup + .extractor_mut() + .extract_field(PhantomData::); + assert_eq!(unique.ok(), Some(&mut Nil)); + + let text = String::from("key"); + let lookup: Lookup<'_, u64> = Lookup::Borrowed(&text); + assert_eq!( + lookup + .to_extractor() + .extract_field(PhantomData::) + .ok(), + Some("key") + ); +} + +/// The operations value dispatch runs on a payload. Value dispatch sees only the +/// payload, so every empty variant reaches the one `Nil` impl. +pub trait Describe { + fn describe(&self) -> String; + + fn bump(&mut self); + + fn into_count(self) -> u64; +} + +impl Describe for Count { + fn describe(&self) -> String { + format!("active {}", self.0) + } + + fn bump(&mut self) { + self.0 += 1; + } + + fn into_count(self) -> u64 { + self.0 + } +} + +impl Describe for Nil { + fn describe(&self) -> String { + "empty".to_owned() + } + + fn bump(&mut self) {} + + fn into_count(self) -> u64 { + 0 + } +} + +#[cgp_computer] +pub fn describe_value(value: &T) -> String { + value.describe() +} + +#[cgp_computer] +pub fn bump_value(value: &mut T) { + value.bump() +} + +#[cgp_computer] +pub fn count_value(value: T) -> u64 { + value.into_count() +} + +#[test] +fn test_value_dispatch_merges_empty_variants() { + let code = PhantomData::<()>; + + assert_eq!( + MatchWithValueHandlersRef::::compute(&App, code, &Status::Active(Count(2))), + "active 2" + ); + assert_eq!( + MatchWithValueHandlersRef::::compute(&App, code, &Status::Closed), + "empty" + ); + assert_eq!( + MatchWithValueHandlersRef::::compute(&App, code, &Status::Archived {}), + "empty" + ); + + let mut active = Status::Active(Count(2)); + MatchWithValueHandlersMut::::compute(&App, code, &mut active); + assert_eq!(active, Status::Active(Count(3))); + + let mut paused = Status::Paused(); + MatchWithValueHandlersMut::::compute(&App, code, &mut paused); + assert_eq!(paused, Status::Paused()); + + assert_eq!( + MatchWithValueHandlers::::compute(&App, code, Status::Active(Count(5))), + 5 + ); + assert_eq!( + MatchWithValueHandlers::::compute(&App, code, Status::Closed), + 0 + ); +} + +pub struct App; + +delegate_components! { + App { + ErrorTypeProviderComponent: UseType, + } +} + +/// Field dispatch sees the variant's tag as well as its payload, so it tells the +/// empty variants apart. +#[cgp_computer] +pub fn variant_name(_field: Field) -> &'static str { + Tag::VALUE +} + +#[test] +fn test_field_dispatch_tells_empty_variants_apart() { + let code = PhantomData::<()>; + + assert_eq!( + MatchWithFieldHandlers::::compute(&App, code, Status::Closed), + "Closed" + ); + assert_eq!( + MatchWithFieldHandlers::::compute(&App, code, Status::Paused()), + "Paused" + ); + assert_eq!( + MatchWithFieldHandlers::::compute(&App, code, Status::Archived {}), + "Archived" + ); +} diff --git a/crates/tests/cgp-tests/tests/extensible_variants/mod.rs b/crates/tests/cgp-tests/tests/extensible_variants/mod.rs index 3487754f5..7c144848d 100644 --- a/crates/tests/cgp-tests/tests/extensible_variants/mod.rs +++ b/crates/tests/cgp-tests/tests/extensible_variants/mod.rs @@ -19,6 +19,7 @@ pub mod has_fields_enum_shapes; // payloads. pub mod derive_cgp_data; pub mod derive_cgp_data_empty; +pub mod derive_cgp_data_empty_variants; pub mod derive_cgp_data_generic; pub mod derive_cgp_data_shape; @@ -33,6 +34,10 @@ pub mod from_variant_derive; // with. A plain behavioral test — the expansion shape is pinned above. pub mod derive_cgp_data_lifetime; +// Variants with no fields (`V`, `V()`, `V {}`), carrying the payload `Nil` +// through construction, extraction, casts, and both kinds of dispatch. +pub mod empty_variants; + // Dispatching an extensible-variant input to per-variant handlers (the derives // here are plain scaffolding — the dispatch combinators are owned elsewhere). pub mod shape_dispatch;