From f656ca5b728cde9559602ebeac47969839edcf9d Mon Sep 17 00:00:00 2001 From: David Spies Date: Tue, 23 Jun 2026 13:05:02 -0700 Subject: [PATCH 1/3] Add Generic derive support for enums --- derives/src/derive_generic.rs | 42 +++++++++++++++++++++++++++++++++-- derives/src/lib.rs | 2 +- proc-macro-helpers/src/lib.rs | 6 +++++ tests/common/mod.rs | 8 +++++++ tests/generic_tests.rs | 42 ++++++++++++++++++++++++++++++++++- 5 files changed, 96 insertions(+), 4 deletions(-) diff --git a/derives/src/derive_generic.rs b/derives/src/derive_generic.rs index 00d13bb4a..c6a207e9d 100644 --- a/derives/src/derive_generic.rs +++ b/derives/src/derive_generic.rs @@ -1,11 +1,12 @@ use frunk_proc_macro_helpers::*; use proc_macro::TokenStream; use quote::ToTokens; +use std::iter::repeat; use syn::Data; /// Given an AST, returns an implementation of Generic using HList /// -/// Only works with Structs and Tuple Structs +/// Works with structs, tuple structs, and enums. pub fn impl_generic(input: TokenStream) -> impl ToTokens { let ast = to_ast(input); let name = &ast.ident; @@ -39,7 +40,44 @@ pub fn impl_generic(input: TokenStream) -> impl ToTokens { } } } - _ => panic!("Only Structs are supported. Enums/Unions cannot be turned into Generics."), + Data::Enum(ref data) => { + let variant_bindings = VariantBindings::new(&data.variants); + let repr_type = &variant_bindings.build_coprod_type(VariantBinding::build_hlist_type); + let coprod_constrs = + &variant_bindings.build_coprod_constrs(VariantBinding::build_hlist_constr); + let coprod_unreachable = &variant_bindings.build_coprod_unreachable_arm(false); + let type_constrs1 = + &variant_bindings.build_variant_constrs(VariantBinding::build_type_constr); + let type_constrs2 = type_constrs1; + let name_it1 = repeat(name); + let name_it2 = repeat(name); + + quote! { + #[allow(non_snake_case, non_camel_case_types)] + impl #impl_generics ::frunk_core::generic::Generic for #name #ty_generics #where_clause { + + type Repr = #repr_type; + + fn into(self) -> Self::Repr { + match self { + #( + #name_it1 :: #type_constrs1 => #coprod_constrs, + )* + } + } + + fn from(r: Self::Repr) -> Self { + match r { + #( + #coprod_constrs => #name_it2 :: #type_constrs2, + )* + #coprod_unreachable + } + } + } + } + } + _ => panic!("Only Structs and Enums can be turned into Generics."), }; // print!("{}", tree); diff --git a/derives/src/lib.rs b/derives/src/lib.rs index 02e78c2fd..d90b053ca 100644 --- a/derives/src/lib.rs +++ b/derives/src/lib.rs @@ -26,7 +26,7 @@ use crate::derive_labelled_generic::impl_labelled_generic; use quote::ToTokens; /// Derives a Generic instance based on HList for -/// a given Struct or Tuple Struct +/// a given struct, tuple struct, or enum. #[proc_macro_derive(Generic)] pub fn generic(input: TokenStream) -> TokenStream { // Build the impl diff --git a/proc-macro-helpers/src/lib.rs b/proc-macro-helpers/src/lib.rs index 94fbac343..2fb323cba 100644 --- a/proc-macro-helpers/src/lib.rs +++ b/proc-macro-helpers/src/lib.rs @@ -397,6 +397,12 @@ impl VariantBinding { .build_hlist_constr(FieldBinding::build_field_pat), ) } + pub fn build_hlist_type(&self) -> TokenStream2 { + self.fields.build_hlist_type(FieldBinding::build_type) + } + pub fn build_hlist_constr(&self) -> TokenStream2 { + self.fields.build_hlist_constr(FieldBinding::build) + } } pub struct VariantBindings { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0e15e9261..0580ee788 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -80,6 +80,14 @@ pub struct HasKeyword2Embedder { #[derive(Generic, Debug, PartialEq, Eq)] pub struct TupleStruct<'a>(pub &'a str, pub i32); +#[allow(dead_code)] // Not actually dead -- used in tests but clippy can't find it +#[derive(Generic, PartialEq, Eq, Debug)] +pub enum GenericEnum { + VariantA, + VariantB(i32), + VariantC { x: String, y: bool }, +} + #[derive(LabelledGeneric)] pub struct NormalUser<'a> { pub first_name: &'a str, diff --git a/tests/generic_tests.rs b/tests/generic_tests.rs index 574877cdd..5120c96ce 100644 --- a/tests/generic_tests.rs +++ b/tests/generic_tests.rs @@ -1,4 +1,4 @@ -use frunk::{convert_from, from_generic, into_generic}; +use frunk::{convert_from, from_generic, into_generic, Coproduct}; use frunk_core::hlist; mod common; @@ -25,6 +25,29 @@ fn test_tuple_struct_from_generic() { assert_eq!(p, TupleStruct("Drumpty", 3)); } +#[test] +fn test_enum_from_generic() { + let variant_a = Coproduct::inject(hlist![]); + let variant_b = Coproduct::inject(hlist![42i32]); + let variant_c = Coproduct::inject(hlist!["test".to_string(), true]); + + assert_eq!( + from_generic::(variant_a), + GenericEnum::VariantA, + ); + assert_eq!( + from_generic::(variant_b), + GenericEnum::VariantB(42), + ); + assert_eq!( + from_generic::(variant_c), + GenericEnum::VariantC { + x: "test".into(), + y: true, + }, + ); +} + #[test] fn test_struct_into_generic() { let p = Person { @@ -36,6 +59,23 @@ fn test_struct_into_generic() { assert_eq!(h, hlist!("Humpty", "Drumpty", 3)); } +#[test] +fn test_enum_into_generic() { + let variant_a = into_generic(GenericEnum::VariantA); + let variant_b = into_generic(GenericEnum::VariantB(42)); + let variant_c = into_generic(GenericEnum::VariantC { + x: "test".into(), + y: true, + }); + + assert_eq!(variant_a, Coproduct::inject(hlist![])); + assert_eq!(variant_b, Coproduct::inject(hlist![42i32])); + assert_eq!( + variant_c, + Coproduct::inject(hlist!["test".to_string(), true]) + ); +} + #[test] fn test_struct_conversion() { let a = Strategist { From 5f11d110c37a143025dd228cd3a5bea7479d1ea2 Mon Sep 17 00:00:00 2001 From: David Spies Date: Tue, 23 Jun 2026 13:05:13 -0700 Subject: [PATCH 2/3] Add Generic and LabelledGeneric impls for std enums --- core/src/generic.rs | 71 +++++++++++++++++++++++ core/src/labelled.rs | 122 +++++++++++++++++++++++++++++++++++++++- tests/generic_tests.rs | 39 ++++++++++++- tests/labelled_tests.rs | 58 +++++++++++++++++++ 4 files changed, 288 insertions(+), 2 deletions(-) diff --git a/core/src/generic.rs b/core/src/generic.rs index 6036a5129..922b6cb0e 100644 --- a/core/src/generic.rs +++ b/core/src/generic.rs @@ -31,6 +31,9 @@ //! let d_person: DomainPerson = frunk::convert_from(a_person); // done //! # } +use crate::coproduct::Coproduct; +use crate::hlist::HNil; + /// A trait that converts from a type to a generic representation. /// /// For the most part, you should be using the derivation that is available @@ -120,6 +123,74 @@ pub trait Generic { } } +type UnaryVariant = crate::HList!(T); +type GenericOptionRepr = crate::Coprod!(HNil, UnaryVariant); +type GenericResultRepr = crate::Coprod!(UnaryVariant, UnaryVariant); +type GenericBoolRepr = crate::Coprod!(HNil, HNil); + +impl Generic for Option { + type Repr = GenericOptionRepr; + + #[inline(always)] + fn into(self) -> Self::Repr { + match self { + None => Coproduct::Inl(crate::hlist![]), + Some(value) => Coproduct::Inr(Coproduct::Inl(crate::hlist![value])), + } + } + + #[inline(always)] + fn from(repr: Self::Repr) -> Self { + match repr { + Coproduct::Inl(crate::hlist_pat![]) => None, + Coproduct::Inr(Coproduct::Inl(crate::hlist_pat![value])) => Some(value), + Coproduct::Inr(Coproduct::Inr(cnil)) => match cnil {}, + } + } +} + +impl Generic for Result { + type Repr = GenericResultRepr; + + #[inline(always)] + fn into(self) -> Self::Repr { + match self { + Ok(value) => Coproduct::Inl(crate::hlist![value]), + Err(value) => Coproduct::Inr(Coproduct::Inl(crate::hlist![value])), + } + } + + #[inline(always)] + fn from(repr: Self::Repr) -> Self { + match repr { + Coproduct::Inl(crate::hlist_pat![value]) => Ok(value), + Coproduct::Inr(Coproduct::Inl(crate::hlist_pat![value])) => Err(value), + Coproduct::Inr(Coproduct::Inr(cnil)) => match cnil {}, + } + } +} + +impl Generic for bool { + type Repr = GenericBoolRepr; + + #[inline(always)] + fn into(self) -> Self::Repr { + match self { + false => Coproduct::Inl(crate::hlist![]), + true => Coproduct::Inr(Coproduct::Inl(crate::hlist![])), + } + } + + #[inline(always)] + fn from(repr: Self::Repr) -> Self { + match repr { + Coproduct::Inl(crate::hlist_pat![]) => false, + Coproduct::Inr(Coproduct::Inl(crate::hlist_pat![])) => true, + Coproduct::Inr(Coproduct::Inr(cnil)) => match cnil {}, + } + } +} + /// Given a generic representation `Repr` of a `Dst`, returns `Dst`. pub fn from_generic(repr: Repr) -> Dst where diff --git a/core/src/labelled.rs b/core/src/labelled.rs index 1506a2f67..fff16cb2b 100644 --- a/core/src/labelled.rs +++ b/core/src/labelled.rs @@ -147,9 +147,11 @@ //! # } //! ``` +use crate::coproduct::Coproduct; use crate::hlist::*; use crate::indices::*; use crate::traits::ToRef; +use chars::*; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -282,6 +284,125 @@ where } } +type NoneLabel = (N, o, n, e); +type SomeLabel = (S, o, m, e); +type OkLabel = (O, k); +type ErrLabel = (E, r, r); +type FalseLabel = (f, a, l, s, e); +type TrueLabel = (t, r, u, e); +type TupleField0 = (__, _0); +type UnitVariant = Field; +type UnaryTupleVariant = Field)>; +type LabelledOptionRepr = + crate::Coprod!(UnitVariant, UnaryTupleVariant); +type LabelledResultRepr = + crate::Coprod!(UnaryTupleVariant, UnaryTupleVariant); +type LabelledBoolRepr = crate::Coprod!(UnitVariant, UnitVariant); + +#[inline(always)] +fn labelled_tuple_field_0(value: T) -> Field { + crate::field!(TupleField0, value, "_0") +} + +#[inline(always)] +fn labelled_unit_variant(name: &'static str) -> UnitVariant { + crate::field!(Name, crate::hlist![], name) +} + +#[inline(always)] +fn labelled_unary_tuple_variant( + name: &'static str, + value: T, +) -> UnaryTupleVariant { + crate::field!(Name, crate::hlist![labelled_tuple_field_0(value)], name) +} + +impl LabelledGeneric for Option { + type Repr = LabelledOptionRepr; + + #[inline(always)] + fn into(self) -> Self::Repr { + match self { + None => Coproduct::Inl(labelled_unit_variant::("None")), + Some(value) => Coproduct::Inr(Coproduct::Inl(labelled_unary_tuple_variant::< + SomeLabel, + _, + >("Some", value))), + } + } + + #[inline(always)] + fn from(repr: Self::Repr) -> Self { + match repr { + Coproduct::Inl(Field { + value: crate::hlist_pat![], + .. + }) => None, + Coproduct::Inr(Coproduct::Inl(Field { + value: crate::hlist_pat![Field { value, .. }], + .. + })) => Some(value), + Coproduct::Inr(Coproduct::Inr(cnil)) => match cnil {}, + } + } +} + +impl LabelledGeneric for Result { + type Repr = LabelledResultRepr; + + #[inline(always)] + fn into(self) -> Self::Repr { + match self { + Ok(value) => Coproduct::Inl(labelled_unary_tuple_variant::("Ok", value)), + Err(value) => Coproduct::Inr(Coproduct::Inl( + labelled_unary_tuple_variant::("Err", value), + )), + } + } + + #[inline(always)] + fn from(repr: Self::Repr) -> Self { + match repr { + Coproduct::Inl(Field { + value: crate::hlist_pat![Field { value, .. }], + .. + }) => Ok(value), + Coproduct::Inr(Coproduct::Inl(Field { + value: crate::hlist_pat![Field { value, .. }], + .. + })) => Err(value), + Coproduct::Inr(Coproduct::Inr(cnil)) => match cnil {}, + } + } +} + +impl LabelledGeneric for bool { + type Repr = LabelledBoolRepr; + + #[inline(always)] + fn into(self) -> Self::Repr { + match self { + false => Coproduct::Inl(labelled_unit_variant::("false")), + true => Coproduct::Inr(Coproduct::Inl(labelled_unit_variant::("true"))), + } + } + + #[inline(always)] + fn from(repr: Self::Repr) -> Self { + match repr { + Coproduct::Inl(Field { + value: crate::hlist_pat![], + .. + }) => false, + Coproduct::Inr(Coproduct::Inl(Field { + value: crate::hlist_pat![], + .. + })) => true, + Coproduct::Inr(Coproduct::Inr(cnil)) => match cnil {}, + } + } +} + /// Given a labelled generic representation of a `Dst`, returns `Dst` pub fn from_labelled_generic(repr: Repr) -> Dst where @@ -968,7 +1089,6 @@ where #[cfg(test)] mod tests { - use super::chars::*; use super::*; use alloc::collections::{LinkedList, VecDeque}; use alloc::{boxed::Box, format, string::ToString, vec, vec::Vec}; diff --git a/tests/generic_tests.rs b/tests/generic_tests.rs index 5120c96ce..18118b2dc 100644 --- a/tests/generic_tests.rs +++ b/tests/generic_tests.rs @@ -1,4 +1,4 @@ -use frunk::{convert_from, from_generic, into_generic, Coproduct}; +use frunk::{convert_from, from_generic, into_generic, Coproduct, Generic}; use frunk_core::hlist; mod common; @@ -76,6 +76,43 @@ fn test_enum_into_generic() { ); } +#[test] +fn test_option_generic() { + let none_repr = into_generic(None::); + let some_repr = into_generic(Some(42i32)); + + assert_eq!(none_repr, Coproduct::Inl(hlist![])); + assert_eq!(some_repr, Coproduct::Inr(Coproduct::Inl(hlist![42i32]))); + assert_eq!(from_generic::, _>(none_repr), None); + assert_eq!(from_generic::, _>(some_repr), Some(42)); +} + +#[test] +fn test_result_generic() { + let ok_repr = into_generic(Ok::(42)); + let err_repr = into_generic(Err::("error")); + + assert_eq!(ok_repr, Coproduct::Inl(hlist![42i32])); + assert_eq!(err_repr, Coproduct::Inr(Coproduct::Inl(hlist!["error"]))); + assert_eq!(from_generic::, _>(ok_repr), Ok(42)); + assert_eq!(from_generic::, _>(err_repr), Err("error")); +} + +#[test] +fn test_bool_generic() { + type BoolRepr = ::Repr; + + let false_repr: BoolRepr = into_generic(false); + let true_repr: BoolRepr = into_generic(true); + let expected_false: BoolRepr = Coproduct::Inl(hlist![]); + let expected_true: BoolRepr = Coproduct::Inr(Coproduct::Inl(hlist![])); + + assert_eq!(false_repr, expected_false); + assert_eq!(true_repr, expected_true); + assert!(!from_generic::(expected_false)); + assert!(from_generic::(expected_true)); +} + #[test] fn test_struct_conversion() { let a = Strategist { diff --git a/tests/labelled_tests.rs b/tests/labelled_tests.rs index 45a9621ca..fa8fda97a 100644 --- a/tests/labelled_tests.rs +++ b/tests/labelled_tests.rs @@ -240,6 +240,64 @@ fn test_enum_into_labelled_generic() { ); } +#[test] +fn test_option_labelled_generic() { + let none_repr = into_labelled_generic(None::); + let some_repr = into_labelled_generic(Some(42i32)); + + assert_eq!(none_repr, Coproduct::Inl(field!((N, o, n, e), hlist![]))); + assert_eq!( + some_repr, + Coproduct::Inr(Coproduct::Inl(field!( + (S, o, m, e), + hlist![field!((__, _0), 42i32, "_0")] + ))) + ); + assert_eq!(from_labelled_generic::, _>(none_repr), None); + assert_eq!(from_labelled_generic::, _>(some_repr), Some(42)); +} + +#[test] +fn test_result_labelled_generic() { + let ok_repr = into_labelled_generic(Ok::(42)); + let err_repr = into_labelled_generic(Err::("error")); + + assert_eq!( + ok_repr, + Coproduct::Inl(field!((O, k), hlist![field!((__, _0), 42i32, "_0")])) + ); + assert_eq!( + err_repr, + Coproduct::Inr(Coproduct::Inl(field!( + (E, r, r), + hlist![field!((__, _0), "error", "_0")] + ))) + ); + assert_eq!( + from_labelled_generic::, _>(ok_repr), + Ok(42) + ); + assert_eq!( + from_labelled_generic::, _>(err_repr), + Err("error") + ); +} + +#[test] +fn test_bool_labelled_generic() { + type BoolRepr = ::Repr; + + let false_repr: BoolRepr = into_labelled_generic(false); + let true_repr: BoolRepr = into_labelled_generic(true); + let expected_false: BoolRepr = Coproduct::Inl(field!((f, a, l, s, e), hlist![])); + let expected_true: BoolRepr = Coproduct::Inr(Coproduct::Inl(field!((t, r, u, e), hlist![]))); + + assert_eq!(false_repr, expected_false); + assert_eq!(true_repr, expected_true); + assert!(!from_labelled_generic::(expected_false)); + assert!(from_labelled_generic::(expected_true)); +} + #[test] fn test_sculpt_enum() { let value = LabelledEnum1::VariantC { From f45b124e85275778ceaf56a445d73af005510328 Mon Sep 17 00:00:00 2001 From: David Spies Date: Wed, 24 Jun 2026 12:26:51 -0700 Subject: [PATCH 3/3] Clarify Generic derive representation docs --- derives/src/derive_generic.rs | 3 ++- derives/src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/derives/src/derive_generic.rs b/derives/src/derive_generic.rs index c6a207e9d..e60ed6c4c 100644 --- a/derives/src/derive_generic.rs +++ b/derives/src/derive_generic.rs @@ -4,7 +4,8 @@ use quote::ToTokens; use std::iter::repeat; use syn::Data; -/// Given an AST, returns an implementation of Generic using HList +/// Given an AST, returns an implementation of Generic using an HList +/// representation for structs and a Coproduct of payload HLists for enums. /// /// Works with structs, tuple structs, and enums. pub fn impl_generic(input: TokenStream) -> impl ToTokens { diff --git a/derives/src/lib.rs b/derives/src/lib.rs index d90b053ca..785503da9 100644 --- a/derives/src/lib.rs +++ b/derives/src/lib.rs @@ -25,8 +25,8 @@ use crate::derive_labelled_generic::impl_labelled_generic; use quote::ToTokens; -/// Derives a Generic instance based on HList for -/// a given struct, tuple struct, or enum. +/// Derives a Generic instance based on HList for structs and +/// Coproducts of payload HLists for enums. #[proc_macro_derive(Generic)] pub fn generic(input: TokenStream) -> TokenStream { // Build the impl