diff --git a/clvm-derive/src/from_clvm.rs b/clvm-derive/src/from_clvm.rs index 9af018ae8..cf3ef9b4a 100644 --- a/clvm-derive/src/from_clvm.rs +++ b/clvm-derive/src/from_clvm.rs @@ -1,66 +1,311 @@ -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, Literal, Span, TokenStream}; use quote::quote; use syn::{ - parse_quote, spanned::Spanned, Data, DeriveInput, Fields, GenericParam, Type, TypeParam, + parse_quote, spanned::Spanned, Data, DeriveInput, Expr, Fields, FieldsNamed, FieldsUnnamed, + GenericParam, Type, }; -use crate::helpers::{add_trait_bounds, parse_args, Repr}; +use crate::{ + helpers::{add_trait_bounds, parse_clvm_attr, parse_int_repr, Repr}, + macros::{repr_macros, Macros}, +}; -pub fn from_clvm(mut ast: DeriveInput) -> TokenStream { - let args = parse_args(&ast.attrs); - let crate_name = quote!(clvm_traits); +#[derive(Default)] +struct FieldInfo { + field_types: Vec, + field_names: Vec, + initializer: TokenStream, +} + +struct VariantInfo { + name: Ident, + discriminant: Expr, + field_info: FieldInfo, + macros: Macros, +} - let field_types: Vec; - let field_names: Vec; - let initializer: TokenStream; +pub fn from_clvm(ast: DeriveInput) -> TokenStream { + let clvm_attr = parse_clvm_attr(&ast.attrs); + let crate_name = quote!(clvm_traits); match &ast.data { - Data::Struct(data_struct) => match &data_struct.fields { - Fields::Named(fields) => { - let fields = &fields.named; - field_types = fields.iter().map(|field| field.ty.clone()).collect(); - field_names = fields - .iter() - .map(|field| field.ident.clone().unwrap()) - .collect(); - initializer = quote!(Self { #( #field_names, )* }); + Data::Struct(data_struct) => { + if clvm_attr.untagged { + panic!("cannot use `untagged` on a struct"); + } + let macros = repr_macros(&crate_name, clvm_attr.expect_repr()); + let field_info = fields(&data_struct.fields); + impl_for_struct(&crate_name, &ast, ¯os, &field_info) + } + Data::Enum(data_enum) => { + if !clvm_attr.untagged && clvm_attr.repr == Some(Repr::Curry) { + panic!("cannot use `curry` on a tagged enum, since unlike other representations, each argument is wrapped"); } - Fields::Unnamed(fields) => { - let fields = &fields.unnamed; - field_types = fields.iter().map(|field| field.ty.clone()).collect(); - field_names = fields - .iter() - .enumerate() - .map(|(i, field)| Ident::new(&format!("field_{i}"), field.span())) - .collect(); - initializer = quote!(Self( #( #field_names, )* )); + + let mut next_discriminant: Expr = parse_quote!(0); + let mut variants = Vec::new(); + + for variant in data_enum.variants.iter() { + let field_info = fields(&variant.fields); + let variant_clvm_attr = parse_clvm_attr(&variant.attrs); + + if variant_clvm_attr.untagged { + panic!("cannot use `untagged` on an enum variant"); + } + + let repr = variant_clvm_attr + .repr + .unwrap_or_else(|| clvm_attr.expect_repr()); + if !clvm_attr.untagged && repr == Repr::Curry { + panic!("cannot use `curry` on a tagged enum variant, since unlike other representations, each argument is wrapped"); + } + + let macros = repr_macros(&crate_name, repr); + let variant_info = VariantInfo { + name: variant.ident.clone(), + discriminant: variant + .discriminant + .as_ref() + .map(|(_, discriminant)| { + next_discriminant = parse_quote!(#discriminant + 1); + discriminant.clone() + }) + .unwrap_or_else(|| { + let discriminant = next_discriminant.clone(); + next_discriminant = parse_quote!(#next_discriminant + 1); + discriminant + }), + field_info, + macros, + }; + variants.push(variant_info); + } + + if clvm_attr.untagged { + impl_for_untagged_enum(&crate_name, &ast, &variants) + } else { + let int_repr = parse_int_repr(&ast.attrs); + impl_for_enum(&crate_name, &ast, &int_repr, &variants) } - Fields::Unit => panic!("unit structs are not supported"), - }, - _ => panic!("expected struct with named or unnamed fields"), + } + Data::Union(_union) => panic!("cannot derive `FromClvm` for a union"), + } +} + +fn fields(fields: &Fields) -> FieldInfo { + match fields { + Fields::Named(fields) => named_fields(fields), + Fields::Unnamed(fields) => unnamed_fields(fields), + Fields::Unit => FieldInfo::default(), + } +} + +fn named_fields(fields: &FieldsNamed) -> FieldInfo { + let fields = &fields.named; + let field_types = fields.iter().map(|field| field.ty.clone()).collect(); + let field_names: Vec = fields + .iter() + .map(|field| field.ident.clone().unwrap()) + .collect(); + let initializer = quote!({ #( #field_names, )* }); + + FieldInfo { + field_types, + field_names, + initializer, + } +} + +fn unnamed_fields(fields: &FieldsUnnamed) -> FieldInfo { + let fields = &fields.unnamed; + let field_types = fields.iter().map(|field| field.ty.clone()).collect(); + let field_names: Vec = fields + .iter() + .enumerate() + .map(|(i, field)| Ident::new(&format!("field_{i}"), field.span())) + .collect(); + let initializer = quote!(( #( #field_names, )* )); + + FieldInfo { + field_types, + field_names, + initializer, + } +} + +fn impl_for_struct( + crate_name: &TokenStream, + ast: &DeriveInput, + Macros { + match_macro, + destructure_macro, + .. + }: &Macros, + FieldInfo { + field_types, + field_names, + initializer, + }: &FieldInfo, +) -> TokenStream { + let node_name = Ident::new("Node", Span::mixed_site()); + + let body = quote! { + let #destructure_macro!( #( #field_names, )* ) = + <#match_macro!( #( #field_types ),* ) + as #crate_name::FromClvm<#node_name>>::from_clvm(decoder, node)?; + Ok(Self #initializer) }; - let struct_name = &ast.ident; - - // `match_macro` decodes a nested tuple containing each of the struct field types within. - // `destructure_macro` destructures the values into the field names, to be stored in the struct. - let (match_macro, destructure_macro) = match args.repr { - Repr::List => ( - quote!( #crate_name::match_list ), - quote!( #crate_name::destructure_list ), - ), - Repr::Tuple => ( - quote!( #crate_name::match_tuple ), - quote!( #crate_name::destructure_tuple ), - ), - Repr::Curry => ( - quote!( #crate_name::match_curried_args ), - quote!( #crate_name::destructure_curried_args ), - ), + generate_from_clvm(crate_name, ast, &node_name, &body) +} + +fn impl_for_enum( + crate_name: &TokenStream, + ast: &DeriveInput, + int_repr: &Ident, + variants: &[VariantInfo], +) -> TokenStream { + let type_name = Literal::string(&ast.ident.to_string()); + let node_name = Ident::new("Node", Span::mixed_site()); + + let mut discriminant_definitions = Vec::new(); + let mut has_initializers = false; + + let variant_bodies = variants + .iter() + .enumerate() + .map(|(i, variant_info)| { + let VariantInfo { + name, + discriminant, + field_info, + macros, + } = variant_info; + + let FieldInfo { + field_types, + field_names, + initializer, + } = field_info; + + let Macros { + match_macro, + destructure_macro, + .. + } = macros; + + let discriminant_ident = Ident::new(&format!("VALUE_{}", i), Span::mixed_site()); + discriminant_definitions.push(quote! { + const #discriminant_ident: #int_repr = #discriminant; + }); + + if initializer.is_empty() { + quote! { + #discriminant_ident => { + Ok(Self::#name) + } + } + } else { + has_initializers = true; + quote! { + #discriminant_ident => { + let #destructure_macro!( #( #field_names ),* ) = + <#match_macro!( #( #field_types ),* ) + as #crate_name::FromClvm<#node_name>>::from_clvm(decoder, args.0)?; + Ok(Self::#name #initializer) + } + } + } + }) + .collect::>(); + + let parse_value = if has_initializers { + quote! { + let (value, args) = <(#int_repr, #crate_name::Raw<#node_name>)>::from_clvm(decoder, node)?; + } + } else { + quote! { + let value = #int_repr::from_clvm(decoder, node)?; + } }; + let body = quote! { + #parse_value + + #( #discriminant_definitions )* + + match value { + #( #variant_bodies )* + _ => Err(#crate_name::FromClvmError::Custom( + format!("failed to match any enum variants of `{}`", #type_name) + )) + } + }; + + generate_from_clvm(crate_name, ast, &node_name, &body) +} + +fn impl_for_untagged_enum( + crate_name: &TokenStream, + ast: &DeriveInput, + variants: &[VariantInfo], +) -> TokenStream { + let type_name = Literal::string(&ast.ident.to_string()); let node_name = Ident::new("Node", Span::mixed_site()); + let variant_bodies = variants + .iter() + .map(|variant_info| { + let VariantInfo { + name, + field_info, + macros, + .. + } = variant_info; + + let FieldInfo { + field_types, + field_names, + initializer, + } = field_info; + + let Macros { + match_macro, + destructure_macro, + .. + } = macros; + + quote! { + if let Ok(#destructure_macro!( #( #field_names ),* )) = + <#match_macro!( #( #field_types ),* ) + as #crate_name::FromClvm<#node_name>>::from_clvm(decoder, decoder.clone_node(&node)) + { + return Ok(Self::#name #initializer); + } + } + }) + .collect::>(); + + let body = quote! { + #( #variant_bodies )* + + Err(#crate_name::FromClvmError::Custom( + format!("failed to match any enum variants of `{}`", #type_name) + )) + }; + + generate_from_clvm(crate_name, ast, &node_name, &body) +} + +fn generate_from_clvm( + crate_name: &TokenStream, + ast: &DeriveInput, + node_name: &Ident, + body: &TokenStream, +) -> TokenStream { + let mut ast = ast.clone(); + let type_name = ast.ident; + add_trait_bounds( &mut ast.generics, parse_quote!(#crate_name::FromClvm<#node_name>), @@ -71,18 +316,18 @@ pub fn from_clvm(mut ast: DeriveInput) -> TokenStream { ast.generics .params - .push(GenericParam::Type(TypeParam::from(node_name.clone()))); + .push(GenericParam::Type(node_name.clone().into())); let (impl_generics, _, _) = ast.generics.split_for_impl(); quote! { #[automatically_derived] - impl #impl_generics #crate_name::FromClvm<#node_name> for #struct_name #ty_generics #where_clause { + impl #impl_generics #crate_name::FromClvm<#node_name> + for #type_name #ty_generics #where_clause { fn from_clvm( decoder: &impl #crate_name::ClvmDecoder, node: #node_name, ) -> ::std::result::Result { - let #destructure_macro!( #( #field_names, )* ) = <#match_macro!( #( #field_types ),* ) as #crate_name::FromClvm<#node_name>>::from_clvm(decoder, node)?; - Ok(#initializer) + #body } } } diff --git a/clvm-derive/src/helpers.rs b/clvm-derive/src/helpers.rs index 4f126be02..f8deb8faf 100644 --- a/clvm-derive/src/helpers.rs +++ b/clvm-derive/src/helpers.rs @@ -1,7 +1,9 @@ use std::fmt; -use proc_macro2::Ident; -use syn::{punctuated::Punctuated, Attribute, GenericParam, Generics, Token, TypeParamBound}; +use proc_macro2::{Ident, Span}; +use syn::{ + ext::IdentExt, punctuated::Punctuated, Attribute, GenericParam, Generics, Token, TypeParamBound, +}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Repr { @@ -20,20 +22,21 @@ impl fmt::Display for Repr { } } -pub struct ClvmDeriveArgs { - pub repr: Repr, +#[derive(Default)] +pub struct ClvmAttr { + pub repr: Option, + pub untagged: bool, } -pub fn parse_args(attrs: &[Attribute]) -> ClvmDeriveArgs { - let repr = parse_repr(attrs); - ClvmDeriveArgs { - repr: repr - .expect("expected clvm attribute parameter of either `tuple`, `list`, or `curry`"), +impl ClvmAttr { + pub fn expect_repr(&self) -> Repr { + self.repr + .expect("expected clvm attribute parameter of either `tuple`, `list`, or `curry`") } } -pub fn parse_repr(attrs: &[Attribute]) -> Option { - let mut repr: Option = None; +pub fn parse_clvm_attr(attrs: &[Attribute]) -> ClvmAttr { + let mut result = ClvmAttr::default(); for attr in attrs { if let Some(ident) = attr.path().get_ident() { if ident == "clvm" { @@ -42,12 +45,20 @@ pub fn parse_repr(attrs: &[Attribute]) -> Option { .unwrap(); for arg in args { - let existing = repr; + let existing = result.repr; - repr = Some(match arg.to_string().as_str() { + result.repr = Some(match arg.to_string().as_str() { "tuple" => Repr::Tuple, "list" => Repr::List, "curry" => Repr::Curry, + "untagged" => { + if result.untagged { + panic!("`untagged` specified twice"); + } else { + result.untagged = true; + } + continue; + } ident => panic!("unknown argument `{ident}`"), }); @@ -58,7 +69,19 @@ pub fn parse_repr(attrs: &[Attribute]) -> Option { } } } - repr + result +} + +pub fn parse_int_repr(attrs: &[Attribute]) -> Ident { + let mut int_repr: Option = None; + for attr in attrs { + if let Some(ident) = attr.path().get_ident() { + if ident == "repr" { + int_repr = Some(attr.parse_args_with(Ident::parse_any).unwrap()); + } + } + } + int_repr.unwrap_or(Ident::new("isize", Span::call_site())) } pub fn add_trait_bounds(generics: &mut Generics, bound: TypeParamBound) { diff --git a/clvm-derive/src/lib.rs b/clvm-derive/src/lib.rs index 7fe75c181..f2165922c 100644 --- a/clvm-derive/src/lib.rs +++ b/clvm-derive/src/lib.rs @@ -2,6 +2,7 @@ extern crate proc_macro; mod from_clvm; mod helpers; +mod macros; mod to_clvm; use from_clvm::from_clvm; diff --git a/clvm-derive/src/macros.rs b/clvm-derive/src/macros.rs new file mode 100644 index 000000000..632657285 --- /dev/null +++ b/clvm-derive/src/macros.rs @@ -0,0 +1,41 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::helpers::Repr; + +pub struct Macros { + /// Encodes a nested tuple containing each of the field values within. + pub clvm_macro: TokenStream, + + /// Decodes a nested tuple containing each of the field types within. + pub match_macro: TokenStream, + + /// Destructures the values into the field names. + pub destructure_macro: TokenStream, +} + +pub fn repr_macros(crate_name: &TokenStream, repr: Repr) -> Macros { + let (clvm_macro, match_macro, destructure_macro) = match repr { + Repr::List => ( + quote!( #crate_name::clvm_list ), + quote!( #crate_name::match_list ), + quote!( #crate_name::destructure_list ), + ), + Repr::Tuple => ( + quote!( #crate_name::clvm_tuple ), + quote!( #crate_name::match_tuple ), + quote!( #crate_name::destructure_tuple ), + ), + Repr::Curry => ( + quote!( #crate_name::clvm_curried_args ), + quote!( #crate_name::match_curried_args ), + quote!( #crate_name::destructure_curried_args ), + ), + }; + + Macros { + clvm_macro, + match_macro, + destructure_macro, + } +} diff --git a/clvm-derive/src/to_clvm.rs b/clvm-derive/src/to_clvm.rs index a6b41afa6..1463a8638 100644 --- a/clvm-derive/src/to_clvm.rs +++ b/clvm-derive/src/to_clvm.rs @@ -1,53 +1,226 @@ use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use syn::{parse_quote, Data, DeriveInput, Fields, GenericParam, Index, TypeParam}; +use quote::{quote, ToTokens}; +use syn::{ + parse_quote, spanned::Spanned, Data, DeriveInput, Expr, Fields, FieldsNamed, FieldsUnnamed, + GenericParam, Index, +}; -use crate::helpers::{add_trait_bounds, parse_args, Repr}; +use crate::{ + helpers::{add_trait_bounds, parse_clvm_attr, Repr}, + macros::{repr_macros, Macros}, +}; -pub fn to_clvm(mut ast: DeriveInput) -> TokenStream { - let args = parse_args(&ast.attrs); - let crate_name = quote!(clvm_traits); +#[derive(Default)] +struct FieldInfo { + field_names: Vec, + field_accessors: Vec, + initializer: TokenStream, +} + +struct VariantInfo { + name: Ident, + discriminant: Expr, + field_info: FieldInfo, + macros: Macros, +} - let field_names: Vec; +pub fn to_clvm(ast: DeriveInput) -> TokenStream { + let clvm_attr = parse_clvm_attr(&ast.attrs); + let crate_name = quote!(clvm_traits); match &ast.data { - Data::Struct(data_struct) => match &data_struct.fields { - Fields::Named(fields) => { - let fields = &fields.named; - field_names = fields - .iter() - .map(|field| { - let ident = field.ident.clone().unwrap(); - quote!(#ident) - }) - .collect(); + Data::Struct(data_struct) => { + if clvm_attr.untagged { + panic!("cannot use `untagged` on a struct"); } - Fields::Unnamed(fields) => { - let fields = &fields.unnamed; - field_names = fields - .iter() - .enumerate() - .map(|(i, _)| { - let index = Index::from(i); - quote!(#index) - }) - .collect(); + let macros = repr_macros(&crate_name, clvm_attr.expect_repr()); + let field_info = fields(&data_struct.fields); + impl_for_struct(&crate_name, &ast, ¯os, &field_info) + } + Data::Enum(data_enum) => { + if !clvm_attr.untagged && clvm_attr.repr == Some(Repr::Curry) { + panic!("cannot use `curry` on a tagged enum, since unlike other representations, each argument is wrapped"); } - Fields::Unit => panic!("unit structs are not supported"), - }, - _ => panic!("expected struct with named or unnamed fields"), - }; - let struct_name = &ast.ident; + let mut next_discriminant: Expr = parse_quote!(0); + let mut variants = Vec::new(); + + for variant in data_enum.variants.iter() { + let field_info = fields(&variant.fields); + let variant_clvm_attr = parse_clvm_attr(&variant.attrs); + + if variant_clvm_attr.untagged { + panic!("cannot use `untagged` on an enum variant"); + } + + let repr = variant_clvm_attr + .repr + .unwrap_or_else(|| clvm_attr.expect_repr()); + if !clvm_attr.untagged && repr == Repr::Curry { + panic!("cannot use `curry` on a tagged enum variant, since unlike other representations, each argument is wrapped"); + } + + let macros = repr_macros(&crate_name, repr); + let variant_info = VariantInfo { + name: variant.ident.clone(), + discriminant: variant + .discriminant + .as_ref() + .map(|(_, discriminant)| { + next_discriminant = parse_quote!(#discriminant + 1); + discriminant.clone() + }) + .unwrap_or_else(|| { + let discriminant = next_discriminant.clone(); + next_discriminant = parse_quote!(#next_discriminant + 1); + discriminant + }), + field_info, + macros, + }; + variants.push(variant_info); + } + + impl_for_enum(&crate_name, &ast, clvm_attr.untagged, &variants) + } + Data::Union(_union) => panic!("cannot derive `ToClvm` for a union"), + } +} + +fn fields(fields: &Fields) -> FieldInfo { + match fields { + Fields::Named(fields) => named_fields(fields), + Fields::Unnamed(fields) => unnamed_fields(fields), + Fields::Unit => FieldInfo::default(), + } +} + +fn named_fields(fields: &FieldsNamed) -> FieldInfo { + let field_names: Vec = fields + .named + .iter() + .map(|field| field.ident.clone().unwrap()) + .collect(); + let field_accessors = field_names + .iter() + .map(|field_name| field_name.clone().to_token_stream()) + .collect(); + let initializer = quote!({ #( #field_names, )* }); + + FieldInfo { + field_names, + field_accessors, + initializer, + } +} + +fn unnamed_fields(fields: &FieldsUnnamed) -> FieldInfo { + let field_names: Vec = fields + .unnamed + .iter() + .enumerate() + .map(|(i, field)| Ident::new(&format!("field_{i}"), field.span())) + .collect(); + let field_accessors = field_names + .iter() + .enumerate() + .map(|(i, _)| Index::from(i).to_token_stream()) + .collect(); + let initializer = quote!(( #( #field_names, )* )); + + FieldInfo { + field_names, + field_accessors, + initializer, + } +} - // `list_macro` encodes a nested tuple containing each of the struct field values within. - let list_macro = match args.repr { - Repr::List => quote!( #crate_name::clvm_list ), - Repr::Tuple => quote!( #crate_name::clvm_tuple ), - Repr::Curry => quote!( #crate_name::clvm_curried_args ), +fn impl_for_struct( + crate_name: &TokenStream, + ast: &DeriveInput, + Macros { clvm_macro, .. }: &Macros, + FieldInfo { + field_accessors, .. + }: &FieldInfo, +) -> TokenStream { + let node_name = Ident::new("Node", Span::mixed_site()); + + let body = quote! { + let value = #clvm_macro!( #( &self.#field_accessors ),* ); + #crate_name::ToClvm::to_clvm(&value, encoder) }; + generate_to_clvm(crate_name, ast, &node_name, &body) +} + +fn impl_for_enum( + crate_name: &TokenStream, + ast: &DeriveInput, + untagged: bool, + variants: &[VariantInfo], +) -> TokenStream { let node_name = Ident::new("Node", Span::mixed_site()); + let has_initializers = variants + .iter() + .any(|variant| !variant.field_info.initializer.is_empty()); + + let variant_bodies = variants + .iter() + .map(|variant_info| { + let VariantInfo { + name, + discriminant, + field_info, + macros, + } = variant_info; + + let FieldInfo { + field_names, + initializer, + .. + } = field_info; + + let Macros { clvm_macro, .. } = macros; + + if untagged { + quote! { + Self::#name #initializer => { + #clvm_macro!( #( #field_names ),* ).to_clvm(encoder) + } + } + } else if has_initializers { + quote! { + Self::#name #initializer => { + (#discriminant, #clvm_macro!( #( #field_names ),* )).to_clvm(encoder) + } + } + } else { + quote! { + Self::#name => { + (#discriminant).to_clvm(encoder) + } + } + } + }) + .collect::>(); + + let body = quote! { + match self { + #( #variant_bodies )* + } + }; + + generate_to_clvm(crate_name, ast, &node_name, &body) +} + +fn generate_to_clvm( + crate_name: &TokenStream, + ast: &DeriveInput, + node_name: &Ident, + body: &TokenStream, +) -> TokenStream { + let mut ast = ast.clone(); + let type_name = ast.ident; add_trait_bounds( &mut ast.generics, @@ -59,18 +232,17 @@ pub fn to_clvm(mut ast: DeriveInput) -> TokenStream { ast.generics .params - .push(GenericParam::Type(TypeParam::from(node_name.clone()))); + .push(GenericParam::Type(node_name.clone().into())); let (impl_generics, _, _) = ast.generics.split_for_impl(); quote! { #[automatically_derived] - impl #impl_generics #crate_name::ToClvm<#node_name> for #struct_name #ty_generics #where_clause { + impl #impl_generics #crate_name::ToClvm<#node_name> for #type_name #ty_generics #where_clause { fn to_clvm( &self, encoder: &mut impl #crate_name::ClvmEncoder ) -> ::std::result::Result<#node_name, #crate_name::ToClvmError> { - let value = #list_macro!( #( &self.#field_names ),* ); - #crate_name::ToClvm::to_clvm(&value, encoder) + #body } } } diff --git a/clvm-traits/docs/derive_macros.md b/clvm-traits/docs/derive_macros.md new file mode 100644 index 000000000..96f184366 --- /dev/null +++ b/clvm-traits/docs/derive_macros.md @@ -0,0 +1,218 @@ +As well as the built-in implementations, this library exposes two derive macros +for implementing the `ToClvm` and `FromClvm` traits on structs and enums. +These macros can be used with both named and unnamed structs and enum variants. + +## Representations + +There are multiple ways to encode a sequence of fields in either a struct or an enum variant. +These are referred to as representations and are specified using the `#[clvm(...)]` attribute. +Below are examples of derive macros using each of these representations. +Pick whichever representation fits your use-case the best. + +Note that the syntax `(A . B)` represents a cons-pair with two values, `A` and `B`. +This is how non-atomic values are structured in CLVM. + +### Tuple + +This represents values in an unterminated series of nested cons-pairs. + +For example: + +- `()` is encoded as `()`, since it's not possible to create a cons-pair with no values. +- `(A)` is encoded as `A`, since it's not possible to create a cons-pair with one value. +- `(A, B)` is encoded as `(A . B)`, since it's already a valid cons-pair. +- `(A, B, C)` is encoded as `(A . (B . C))`, since every cons-pair must contain two values. +- `(A, B, C, D)` is encoded as `(A . (B . (C . D)))` for the same reason as above. + +```rust +use clvmr::Allocator; +use clvm_traits::{ToClvm, FromClvm}; + +#[derive(Debug, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(tuple)] +struct Point { + x: i32, + y: i32, +} + +let point = Point { + x: 5, + y: 2, +}; + +let a = &mut Allocator::new(); +let ptr = point.to_clvm(a).unwrap(); +assert_eq!(Point::from_clvm(a, ptr).unwrap(), point); +``` + +### List + +This represents values in a null terminated series of nested cons-pairs, also known as a proper list. + +For example: + +- `()` is encoded as `()`, since it's already a null value. +- `(A)` is encoded as `(A, ())`, since it's null terminated. +- `(A, B)` is encoded as `(A . (B . ()))`, nesting the cons-pairs just like tuples, except with a null terminator. +- `(A, B, C)` is encoded as `(A . (B . (C . ())))` for the same reason. + +Note that the following code is for example purposes only and is not indicative of how to create a secure program. +Using a password like shown in this example is an insecure method of locking coins, but it's effective for learning. + +```rust +use clvmr::Allocator; +use clvm_traits::{ToClvm, FromClvm}; + +#[derive(Debug, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +struct PasswordSolution { + password: String, +} + +let solution = PasswordSolution { + password: "Hello".into(), +}; + +let a = &mut Allocator::new(); +let ptr = solution.to_clvm(a).unwrap(); +assert_eq!(PasswordSolution::from_clvm(a, ptr).unwrap(), solution); +``` + +### Curry + +This represents the argument part of a curried CLVM program. Currying is a method of partially +applying some of the arguments without immediately calling the function. + +For example, `(A, B, C)` is encoded as `(c (q . A) (c (q . B) (c (q . C) 1)))`. Note that the +arguments are quoted and terminated with `1`, which is how partial application is implemented in CLVM. + +You can read more about currying on the [Chia blockchain documentation](https://docs.chia.net/guides/chialisp-currying). + +Note that the following code is for example purposes only and is not indicative of how to create a secure program. +Using a password like shown in this example is an insecure method of locking coins, but it's effective for learning. + +```rust +use clvmr::Allocator; +use clvm_traits::{ToClvm, FromClvm}; + +#[derive(Debug, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +struct PasswordArgs { + password: String, +} + +let args = PasswordArgs { + password: "Hello".into(), +}; + +let a = &mut Allocator::new(); +let ptr = args.to_clvm(a).unwrap(); +assert_eq!(PasswordArgs::from_clvm(a, ptr).unwrap(), args); +``` + +## Enums + +In Rust, enums contain a discriminant, a value used to distinguish between each variant of the enum. +In most cases, the CLVM representation of the enum will need to contain this discriminant as the first argument. +For convenience, this is the behavior when deriving `ToClvm` and `FromClvm` for enums by default. + +### Simple Example + +In this example, since the `tuple` representation is used and the only values are the discriminants, the variants will be encoded as an atom. +Discriminants default to the `isize` type and the first value is `0`. Subsequent values are incremented by `1` by default. + +```rust +use clvmr::Allocator; +use clvm_traits::{ToClvm, FromClvm}; + +#[derive(Debug, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(tuple)] +enum Status { + Pending, + Completed, +} + +let status = Status::Pending; + +let a = &mut Allocator::new(); +let ptr = status.to_clvm(a).unwrap(); +assert_eq!(Status::from_clvm(a, ptr).unwrap(), status); +``` + +### Custom Discriminator + +It's possible to override both the type of the discriminator, and the value. +The `#[repr(...)]` attribute is used by the Rust compiler to allow overriding the discriminator type. +As such, this attribute is also used to change the underlying type used to serialize and deserialize discriminator values. + +```rust +use clvmr::Allocator; +use clvm_traits::{ToClvm, FromClvm}; + +#[derive(Debug, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(tuple)] +#[repr(u8)] +enum Status { + Pending = 36, + Completed = 42, +} + +let status = Status::Pending; + +let a = &mut Allocator::new(); +let ptr = status.to_clvm(a).unwrap(); +assert_eq!(Status::from_clvm(a, ptr).unwrap(), status); +``` + +### Variant Fields + +Of course, you can also include fields on enum variants, and they will be serialized after the discriminator accordingly. +It's also possible to override the representation of an individual variant, as if it were a standalone struct. + +```rust +use clvmr::Allocator; +use clvm_traits::{ToClvm, FromClvm}; + +#[derive(Debug, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +enum SpendMode { + AppendValue { value: i32 }, + + #[clvm(tuple)] + ClearValues, +} + +let mode = SpendMode::AppendValue { + value: 42 +}; + +let a = &mut Allocator::new(); +let ptr = mode.to_clvm(a).unwrap(); +assert_eq!(SpendMode::from_clvm(a, ptr).unwrap(), mode); +``` + +### Untagged Enums + +Often, the discriminator isn't necessary to encode, and you'd prefer to try to match each variant in order until one matches. +This is what `#[clvm(untagged)]` allows you to do. However, due to current limitations, it's not possible to mix this with `#[clvm(curry)]`. + +Note that if there is any ambiguity, the first variant which matches a value will be the resulting value. +For example, if both `A` and `B` are in that order and are the same type, if you serialize a value of `B`, it will be deserialized as `A`. + +```rust +use clvmr::Allocator; +use clvm_traits::{ToClvm, FromClvm}; + +#[derive(Debug, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(tuple, untagged)] +enum Either { + ShortList([i32; 4]), + ExtendedList([i32; 16]), +} + +let value = Either::ShortList([42; 4]); + +let a = &mut Allocator::new(); +let ptr = value.to_clvm(a).unwrap(); +assert_eq!(Either::from_clvm(a, ptr).unwrap(), value); +``` diff --git a/clvm-traits/src/lib.rs b/clvm-traits/src/lib.rs index b25183db2..6b6b93f6d 100644 --- a/clvm-traits/src/lib.rs +++ b/clvm-traits/src/lib.rs @@ -2,40 +2,9 @@ //! This is a library for encoding and decoding Rust values using a CLVM allocator. //! It provides implementations for every fixed-width signed and unsigned integer type, //! as well as many other values in the standard library that would be common to encode. -//! -//! As well as the built-in implementations, this library exposes two derive macros -//! for implementing the `ToClvm` and `FromClvm` traits on structs. They be marked -//! with one of the following encodings: -//! -//! * `#[clvm(tuple)]` for unterminated lists such as `(A . (B . C))`. -//! * `#[clvm(list)]` for proper lists such as `(A B C)`, or in other words `(A . (B . (C . ())))`. -//! * `#[clvm(curry)]` for curried arguments such as `(c (q . A) (c (q . B) (c (q . C) 1)))`. - -#![cfg_attr( - feature = "derive", - doc = r#" -## Derive Example - -```rust -use clvmr::Allocator; -use clvm_traits::{ToClvm, FromClvm}; - -#[derive(Debug, PartialEq, Eq, ToClvm, FromClvm)] -#[clvm(tuple)] -struct Point { - x: i32, - y: i32, -} - -let a = &mut Allocator::new(); - -let point = Point { x: 5, y: 2 }; -let ptr = point.to_clvm(a).unwrap(); -assert_eq!(Point::from_clvm(a, ptr).unwrap(), point); -``` -"# -)] +#![cfg_attr(feature = "derive", doc = "\n\n")] +#![cfg_attr(feature = "derive", doc = include_str!("../docs/derive_macros.md"))] #[cfg(feature = "derive")] pub use clvm_derive::*; @@ -83,6 +52,16 @@ mod tests { assert_eq!(expected, actual); } + fn coerce_into(value: A) -> B + where + A: ToClvm, + B: FromClvm, + { + let a = &mut Allocator::new(); + let ptr = value.to_clvm(a).unwrap(); + B::from_clvm(a, ptr).unwrap() + } + #[test] fn test_tuple() { #[derive(Debug, ToClvm, FromClvm, PartialEq, Eq)] @@ -139,4 +118,98 @@ mod tests { check(NewTypeStruct("XYZ".to_string()), "8358595a"); } + + #[test] + fn test_enum() { + #[derive(Debug, ToClvm, FromClvm, PartialEq, Eq)] + #[clvm(tuple)] + enum Enum { + A(i32), + B { x: i32 }, + C, + } + + check(Enum::A(32), "ff8020"); + check(Enum::B { x: -72 }, "ff0181b8"); + check(Enum::C, "ff0280"); + } + + #[test] + fn test_explicit_enum() { + #[derive(Debug, ToClvm, FromClvm, PartialEq, Eq)] + #[clvm(tuple)] + #[repr(u8)] + enum Enum { + A(i32) = 42, + B { x: i32 } = 34, + C = 11, + } + + check(Enum::A(32), "ff2a20"); + check(Enum::B { x: -72 }, "ff2281b8"); + check(Enum::C, "ff0b80"); + } + + #[test] + fn test_untagged_enum() { + #[derive(Debug, ToClvm, FromClvm, PartialEq, Eq)] + #[clvm(tuple, untagged)] + enum Enum { + A(i32), + + #[clvm(list)] + B { + x: i32, + y: i32, + }, + + #[clvm(curry)] + C { + curried_value: String, + }, + } + + check(Enum::A(32), "20"); + check(Enum::B { x: -72, y: 94 }, "ff81b8ff5e80"); + check( + Enum::C { + curried_value: "Hello".to_string(), + }, + "ff04ffff018548656c6c6fff0180", + ); + } + + #[test] + fn test_untagged_enum_parsing_order() { + #[derive(Debug, ToClvm, FromClvm, PartialEq, Eq)] + #[clvm(tuple, untagged)] + enum Enum { + // This variant is parsed first, so `B` will never be deserialized. + A(i32), + // When `B` is serialized, it will round trip as `A` instead. + B(i32), + // `C` will be deserialized as a fallback when the bytes don't deserialize to a valid `i32`. + C(String), + } + + // This round trips to the same value, since `A` is parsed first. + assert_eq!(coerce_into::(Enum::A(32)), Enum::A(32)); + + // This round trips to `A` instead of `B`, since `A` is parsed first. + assert_eq!(coerce_into::(Enum::B(32)), Enum::A(32)); + + // This round trips to `A` instead of `C`, since the bytes used to represent + // this string are also a valid `i32` value. + assert_eq!( + coerce_into::(Enum::C("Hi".into())), + Enum::A(18537) + ); + + // This round trips to `C` instead of `A`, since the bytes used to represent + // this string exceed the size of `i32`. + assert_eq!( + coerce_into::(Enum::C("Hello, world!".into())), + Enum::C("Hello, world!".into()) + ); + } } diff --git a/clvm-traits/src/macros.rs b/clvm-traits/src/macros.rs index 46b9bcd36..388e45513 100644 --- a/clvm-traits/src/macros.rs +++ b/clvm-traits/src/macros.rs @@ -1,3 +1,4 @@ +/// Converts a list of CLVM values into a series of nested pairs. #[macro_export] macro_rules! clvm_list { () => { @@ -8,8 +9,12 @@ macro_rules! clvm_list { }; } +/// Converts a tuple of CLVM values into a series of nested pairs. #[macro_export] macro_rules! clvm_tuple { + () => { + () + }; ( $first:expr $(,)? ) => { $first }; @@ -18,6 +23,7 @@ macro_rules! clvm_tuple { }; } +/// Quotes a CLVM value. #[macro_export] macro_rules! clvm_quote { ( $value:expr ) => { @@ -25,6 +31,7 @@ macro_rules! clvm_quote { }; } +/// Constructs a sequence of nested pairs that represents a set of curried arguments. #[macro_export] macro_rules! clvm_curried_args { () => { @@ -35,6 +42,7 @@ macro_rules! clvm_curried_args { }; } +/// Creates the type needed to represent a list of CLVM types. #[macro_export] macro_rules! match_list { () => { @@ -45,8 +53,12 @@ macro_rules! match_list { }; } +/// Creates the type needed to represent a tuple of CLVM types. #[macro_export] macro_rules! match_tuple { + () => { + () + }; ( $first:ty $(,)? ) => { $first }; @@ -55,6 +67,7 @@ macro_rules! match_tuple { }; } +/// Creates the type needed to represent a quoted CLVM type. #[macro_export] macro_rules! match_quote { ( $type:ty ) => { @@ -62,6 +75,7 @@ macro_rules! match_quote { }; } +/// Creates the type needed to represent a set of curried arguments. #[macro_export] macro_rules! match_curried_args { () => { @@ -78,6 +92,7 @@ macro_rules! match_curried_args { }; } +/// Deconstructs a CLVM list that has been matched. #[macro_export] macro_rules! destructure_list { () => { @@ -88,8 +103,12 @@ macro_rules! destructure_list { }; } +/// Deconstructs a CLVM tuple that has been matched. #[macro_export] macro_rules! destructure_tuple { + () => { + _ + }; ( $first:pat $(,)? ) => { $first }; @@ -98,6 +117,7 @@ macro_rules! destructure_tuple { }; } +/// Deconstructs a quoted CLVM value that has been matched. #[macro_export] macro_rules! destructure_quote { ( $name:pat ) => { @@ -105,6 +125,7 @@ macro_rules! destructure_quote { }; } +/// Deconstructs a set of curried arguments that has been matched. #[macro_export] macro_rules! destructure_curried_args { () => {