From 4e0264c5827ab713230efeac188e613f7f4b7485 Mon Sep 17 00:00:00 2001 From: bbaldino Date: Sun, 25 May 2025 22:52:46 -0700 Subject: [PATCH 01/10] wip --- impl/src/code_gen/gen_read.rs | 47 ++++-- impl/src/lib.rs | 18 ++- impl/src/model_types.rs | 85 ++++++++++ impl/src/parsely_data/mod.rs | 5 + .../parsely_data/parsely_common_field_data.rs | 18 +++ .../parsely_data/parsely_read_enum_data.rs | 12 ++ .../parsely_data/parsely_read_field_data.rs | 146 ++++++++++++++++++ .../parsely_data/parsely_read_struct_data.rs | 118 ++++++++++++++ .../parsely_data/parsely_read_variant_data.rs | 7 + tests/expand/alignment.expanded.rs | 6 +- 10 files changed, 435 insertions(+), 27 deletions(-) create mode 100644 impl/src/parsely_data/mod.rs create mode 100644 impl/src/parsely_data/parsely_common_field_data.rs create mode 100644 impl/src/parsely_data/parsely_read_enum_data.rs create mode 100644 impl/src/parsely_data/parsely_read_field_data.rs create mode 100644 impl/src/parsely_data/parsely_read_struct_data.rs create mode 100644 impl/src/parsely_data/parsely_read_variant_data.rs diff --git a/impl/src/code_gen/gen_read.rs b/impl/src/code_gen/gen_read.rs index 814bb1a..dff1052 100644 --- a/impl/src/code_gen/gen_read.rs +++ b/impl/src/code_gen/gen_read.rs @@ -4,32 +4,47 @@ use quote::{format_ident, quote}; use crate::{ get_crate_name, model_types::{wrap_read_with_padding_handling, CollectionLimit, TypedFnArgList}, + parsely_data::{ + parsely_read_enum_data::ParselyReadEnumData, + parsely_read_struct_data::ParselyReadStructData, + }, syn_helpers::TypeExts, - ParselyReadData, ParselyReadFieldData, + ParselyReadFieldReceiver, ParselyReadReceiver, }; -pub fn generate_parsely_read_impl(data: ParselyReadData) -> TokenStream { - let struct_name = data.ident; +pub fn generate_parsely_read_impl(data: ParselyReadReceiver) -> TokenStream { if data.data.is_struct() { - generate_parsely_read_impl_struct( - struct_name, - data.data.take_struct().unwrap(), - data.alignment, - data.required_context, - ) + let struct_data = ParselyReadStructData::try_from(data).unwrap(); + generate_parsely_read_impl_for_struct(struct_data) + // generate_parsely_read_impl_struct( + // data.ident, + // data.data.take_struct().unwrap(), + // data.alignment, + // data.required_context, + // ) } else { todo!() } } -fn generate_plain_read(ty: &syn::Type, context_values: &[syn::Expr]) -> TokenStream { +fn generate_parsely_read_impl_for_struct(data: ParselyReadStructData) -> TokenStream { + quote! { + #data + } +} + +fn generate_parsely_read_impl_for_enum(data: ParselyReadEnumData) -> TokenStream { + TokenStream::new() +} + +pub(crate) fn generate_plain_read(ty: &syn::Type, context_values: &[syn::Expr]) -> TokenStream { quote! { #ty::read::(buf, (#(#context_values,)*)) } } -fn generate_collection_read( - limit: CollectionLimit, +pub(crate) fn generate_collection_read( + limit: &CollectionLimit, ty: &syn::Type, context_values: &[syn::Expr], ) -> TokenStream { @@ -66,7 +81,7 @@ fn generate_collection_read( } } -fn wrap_in_optional(when_expr: &syn::Expr, inner: TokenStream) -> TokenStream { +pub(crate) fn wrap_in_optional(when_expr: &syn::Expr, inner: TokenStream) -> TokenStream { quote! { if #when_expr { Some(#inner) @@ -98,7 +113,7 @@ fn wrap_in_optional(when_expr: &syn::Expr, inner: TokenStream) -> TokenStream { /// read should actually be done. /// 7. Finally, if an 'alignment' attribute is present, code is added to detect and consume any /// padding after the read. -fn generate_field_read(field_data: &ParselyReadFieldData) -> TokenStream { +fn generate_field_read(field_data: &ParselyReadFieldReceiver) -> TokenStream { let field_name = field_data .ident .as_ref() @@ -123,7 +138,7 @@ fn generate_field_read(field_data: &ParselyReadFieldData) -> TokenStream { } else { panic!("Collection field '{field_name}' must have either 'count' or 'while' attribute"); }; - output.extend(generate_collection_read(limit, read_type, &context_values)); + output.extend(generate_collection_read(&limit, read_type, &context_values)); } else { output.extend(generate_plain_read(read_type, &context_values)); } @@ -158,7 +173,7 @@ fn generate_field_read(field_data: &ParselyReadFieldData) -> TokenStream { fn generate_parsely_read_impl_struct( struct_name: syn::Ident, - fields: darling::ast::Fields, + fields: darling::ast::Fields, struct_alignment: Option, required_context: Option, ) -> TokenStream { diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 79f9188..27181d2 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -1,6 +1,7 @@ mod code_gen; pub mod error; mod model_types; +pub mod parsely_data; pub mod parsely_read; pub mod parsely_write; mod syn_helpers; @@ -35,7 +36,7 @@ use syn_helpers::TypeExts; #[doc(hidden)] pub fn derive_parsely_read(item: TokenStream) -> std::result::Result { let ast: DeriveInput = syn::parse2(item)?; - let data = ParselyReadData::from_derive_input(&ast)?; + let data = ParselyReadReceiver::from_derive_input(&ast)?; Ok(generate_parsely_read_impl(data)) } @@ -49,7 +50,7 @@ pub fn derive_parsely_write(item: TokenStream) -> std::result::Result, ty: syn::Type, #[darling(flatten)] - common: ParselyCommonFieldData, + common: ParselyCommonFieldReceiver, /// 'count' is required when the field is a collection count: Option, /// 'while' is an alternate option to 'count' to use with a collection field @@ -91,10 +92,11 @@ pub struct ParselyReadFieldData { when: Option, } -impl ParselyReadFieldData { +impl ParselyReadFieldReceiver { /// Get the 'buffer type' of this field (the type that will be used when reading from or /// writing to the buffer): for wrapper types (like [`Option`] or [`Vec`]), this will be the /// inner type. + /// TODO: this will move to the data type and go away from ehre pub(crate) fn buffer_type(&self) -> &syn::Type { if self.ty.is_option() || self.ty.is_collection() { self.ty @@ -128,7 +130,7 @@ pub struct ParselyWriteFieldData { ty: syn::Type, #[darling(flatten)] - common: ParselyCommonFieldData, + common: ParselyCommonFieldReceiver, /// An expression or function call that will be used to update this field in the generated /// `StateSync` implementation for its parent type. @@ -183,11 +185,11 @@ impl ParselyWriteFieldData { #[derive(Debug, FromDeriveInput)] #[darling(attributes(parsely, parsely_read), supports(struct_any, enum_any))] -pub struct ParselyReadData { +pub struct ParselyReadReceiver { ident: syn::Ident, required_context: Option, alignment: Option, - data: ast::Data<(), ParselyReadFieldData>, + data: ast::Data<(), ParselyReadFieldReceiver>, } #[derive(Debug, FromDeriveInput)] diff --git a/impl/src/model_types.rs b/impl/src/model_types.rs index 952b6d8..bde4a91 100644 --- a/impl/src/model_types.rs +++ b/impl/src/model_types.rs @@ -269,6 +269,22 @@ impl MapExpr { }) } + pub(crate) fn to_read_map_tokens2(&self, field_name: &MemberIdent, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let field_name_string = field_name.as_friendly_string(); + let map_expr = &self.0; + // TODO: is there a case where context might be required for reading the 'buffer_type' + // value? + tokens.extend(quote! { + { + let original_value = ::#crate_name::ParselyRead::read::(buf, ()) + .with_context(|| format!("Reading raw value for field '{}'", #field_name_string))?; + (#map_expr)(original_value).into_parsely_result() + .with_context(|| format!("Mapping raw value for field '{}'", #field_name_string)) + } + }) + } + pub(crate) fn to_write_map_tokens(&self, field_name: &syn::Ident, tokens: &mut TokenStream) { let crate_name = get_crate_name(); let field_name_string = field_name.to_string(); @@ -356,6 +372,26 @@ pub(crate) fn wrap_read_with_padding_handling( } } +pub(crate) fn wrap_read_with_padding_handling2( + element_ident: &MemberIdent, + alignment: usize, + inner: TokenStream, +) -> TokenStream { + let bytes_read_before_ident = format_ident!( + "__bytes_read_before_{}_read", + element_ident.as_friendly_string() + ); + quote! { + let #bytes_read_before_ident = buf.remaining_bytes(); + + #inner + + while (#bytes_read_before_ident - buf.remaining_bytes()) % #alignment != 0 { + buf.get_u8().context("consuming padding")?; + } + } +} + pub(crate) fn wrap_write_with_padding_handling( element_ident: &syn::Ident, alignment: usize, @@ -378,3 +414,52 @@ pub(crate) fn wrap_write_with_padding_handling( } } } + +pub(crate) enum MemberIdent { + Named(syn::Ident), + // Unnamed members just have an index + Unnamed(u32), +} + +impl MemberIdent { + /// Create a `MemberIdent` from the given `ident`, if it's `Some` or the given `index` if not. + pub fn from_ident_or_index(ident: Option<&syn::Ident>, index: u32) -> Self { + if let Some(ident) = ident { + MemberIdent::Named(ident.to_owned()) + } else { + MemberIdent::Unnamed(index) + } + } + + pub fn from_ident(ident: &syn::Ident) -> Self { + MemberIdent::Named(ident.to_owned()) + } + + /// Return the value of this `MemberIdent` as a user-friendly String. This version is intended + /// to be used for things like error messages. + pub fn as_friendly_string(&self) -> String { + match self { + MemberIdent::Named(ident) => ident.to_string(), + MemberIdent::Unnamed(index) => format!("Field {index}"), + } + } + + /// Return the value of this `MemberIdent` in the form of a `syn::Ident` that can be used as a + /// local variable. + pub fn as_variable_name(&self) -> syn::Ident { + match self { + MemberIdent::Named(ident) => ident.clone(), + MemberIdent::Unnamed(index) => format_ident!("field_{index}"), + } + } + + /// Return the value of this `MemberIdent` in the form of a `syn::Ident` such that it can be + /// used to access this field inside the containing structure or enum. E.g. for a named + /// variable it will be the field's name, for an unnamed variable it will be the field's index. + pub fn field_name(&self) -> syn::Ident { + match self { + MemberIdent::Named(ident) => ident.clone(), + MemberIdent::Unnamed(index) => format_ident!("{index}"), + } + } +} diff --git a/impl/src/parsely_data/mod.rs b/impl/src/parsely_data/mod.rs new file mode 100644 index 0000000..68aeec1 --- /dev/null +++ b/impl/src/parsely_data/mod.rs @@ -0,0 +1,5 @@ +mod parsely_common_field_data; +pub mod parsely_read_enum_data; +pub mod parsely_read_field_data; +pub mod parsely_read_struct_data; +pub mod parsely_read_variant_data; diff --git a/impl/src/parsely_data/parsely_common_field_data.rs b/impl/src/parsely_data/parsely_common_field_data.rs new file mode 100644 index 0000000..6b0424e --- /dev/null +++ b/impl/src/parsely_data/parsely_common_field_data.rs @@ -0,0 +1,18 @@ +use crate::{model_types::MemberIdent, Assertion, Context, MapExpr}; + +/// Items that are common across `ParselyRead` and `ParselyWrite` fields +pub struct ParselyCommonFieldData { + pub ident: MemberIdent, + /// The field's type + pub ty: syn::Type, + + pub assertion: Option, + /// Values that need to be passed as context to this fields read or write method + pub context: Option, + + /// An optional mapping that will be applied to the read value + pub map: Option, + /// An optional indicator that this field is or needs to be aligned to the given byte alignment + /// via padding. + pub alignment: Option, +} diff --git a/impl/src/parsely_data/parsely_read_enum_data.rs b/impl/src/parsely_data/parsely_read_enum_data.rs new file mode 100644 index 0000000..568c799 --- /dev/null +++ b/impl/src/parsely_data/parsely_read_enum_data.rs @@ -0,0 +1,12 @@ +use crate::TypedFnArgList; + +use super::parsely_read_variant_data::ParselyReadVariantData; + +/// A struct which represents all information needed for generating a `ParselyRead` implementation +/// for a given struct. +pub struct ParselyReadEnumData { + ident: syn::Ident, + required_context: Option, + alignment: Option, + variants: Vec, +} diff --git a/impl/src/parsely_data/parsely_read_field_data.rs b/impl/src/parsely_data/parsely_read_field_data.rs new file mode 100644 index 0000000..1bbf8b5 --- /dev/null +++ b/impl/src/parsely_data/parsely_read_field_data.rs @@ -0,0 +1,146 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{ + code_gen::gen_read::{generate_collection_read, generate_plain_read, wrap_in_optional}, + model_types::{wrap_read_with_padding_handling2, CollectionLimit, MemberIdent}, + ParselyReadFieldReceiver, TypeExts, +}; + +use super::parsely_common_field_data::ParselyCommonFieldData; + +/// A struct which represents all information needed for generating a `ParselyRead` implementation +/// for a given field. +pub struct ParselyReadFieldData { + /// Data common between read and write for fields + pub(crate) common: ParselyCommonFieldData, + /// Required when there's a collection field + pub(crate) collection_limit: Option, + /// Instead of reading the value of this field from the buffer, assign it from the given + /// [`syn::Ident`] + pub(crate) assign_from: Option, + /// 'when' is required when there's an optional field + pub(crate) when: Option, +} + +impl ParselyReadFieldData { + pub(crate) fn from_receiver( + field_ident: MemberIdent, + receiver: ParselyReadFieldReceiver, + ) -> Self { + let collection_limit = if receiver.ty.is_collection() { + if let Some(count) = receiver.count { + Some(CollectionLimit::Count(count)) + } else if let Some(while_pred) = receiver.while_pred { + Some(CollectionLimit::While(while_pred)) + } else { + panic!("Collection type must have 'count' or 'while' attribute"); + } + } else { + None + }; + let when = if receiver.ty.is_option() { + Some( + receiver + .when + .expect("Option field must have 'when' attribute"), + ) + } else { + None + }; + let common = ParselyCommonFieldData { + ident: field_ident, + ty: receiver.ty, + assertion: receiver.common.assertion, + context: receiver.common.context, + map: receiver.common.map, + alignment: receiver.common.alignment, + }; + Self { + common, + collection_limit, + assign_from: receiver.assign_from, + when, + } + } + + /// Get the 'buffer type' of this field (the type that will be used when reading from or + /// writing to the buffer): for wrapper types (like [`Option`] or [`Vec`]), this will be the + /// inner type. + pub(crate) fn buffer_type(&self) -> &syn::Type { + if self.common.ty.is_option() || self.common.ty.is_collection() { + self.common + .ty + .inner_type() + .expect("Option or collection has an inner type") + } else { + &self.common.ty + } + } + + /// Get the context values that need to be passed to the read or write call for this field + pub(crate) fn context_values(&self) -> Vec { + if let Some(ref field_context) = self.common.context { + field_context.expressions(&format!( + "Read context for field '{}'", + self.common.ident.as_friendly_string() + )) + } else { + vec![] + } + } +} + +impl ToTokens for ParselyReadFieldData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut output = TokenStream::new(); + if let Some(ref assign_expr) = self.assign_from { + output.extend(quote! { + ParselyResult::<_>::Ok(#assign_expr) + }); + } else if let Some(ref map_expr) = self.common.map { + map_expr.to_read_map_tokens2(&self.common.ident, &mut output); + } else if self.common.ty.is_collection() { + // We've ensure collection_limit is set in this case elswhere. + let limit = self.collection_limit.as_ref().unwrap(); + output.extend(generate_collection_read( + limit, + &self.common.ty, + &self.context_values(), + )); + } else { + output.extend(generate_plain_read( + self.buffer_type(), + &self.context_values(), + )); + } + + if let Some(ref assertion) = self.common.assertion { + assertion + .to_read_assertion_tokens(&self.common.ident.as_friendly_string(), &mut output); + } + let error_context = format!("Reading field '{}'", self.common.ident.as_friendly_string()); + output.extend(quote! { + .with_context(|| #error_context)? + }); + + output = if self.common.ty.is_option() && self.common.map.is_none() { + // We've ensured 'when' is set in this case elsehwere + let when_expr = self.when.as_ref().unwrap(); + wrap_in_optional(when_expr, output) + } else { + output + }; + + output = if let Some(alignment) = self.common.alignment { + wrap_read_with_padding_handling2(&self.common.ident, alignment, output) + } else { + output + }; + + let field_variable_name = self.common.ident.as_variable_name(); + tokens.extend(quote! { + let #field_variable_name = #output; + }) + } +} diff --git a/impl/src/parsely_data/parsely_read_struct_data.rs b/impl/src/parsely_data/parsely_read_struct_data.rs new file mode 100644 index 0000000..7ad27f6 --- /dev/null +++ b/impl/src/parsely_data/parsely_read_struct_data.rs @@ -0,0 +1,118 @@ +use anyhow::anyhow; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; + +use crate::{ + get_crate_name, + model_types::{wrap_read_with_padding_handling2, MemberIdent}, + ParselyReadReceiver, TypedFnArgList, +}; + +use super::parsely_read_field_data::ParselyReadFieldData; + +/// A struct which represents all information needed for generating a `ParselyRead` implementation +/// for a given struct. +pub(crate) struct ParselyReadStructData { + pub(crate) ident: syn::Ident, + pub(crate) style: darling::ast::Style, + pub(crate) required_context: Option, + pub(crate) alignment: Option, + pub(crate) fields: Vec, +} + +impl TryFrom for ParselyReadStructData { + type Error = anyhow::Error; + + fn try_from(value: ParselyReadReceiver) -> Result { + let (style, struct_receiver_fields) = value + .data + .take_struct() + .ok_or(anyhow!("Not a struct"))? + .split(); + let data_fields = struct_receiver_fields + .into_iter() + .enumerate() + .map(|(field_index, field)| { + let ident = + MemberIdent::from_ident_or_index(field.ident.as_ref(), field_index as u32); + ParselyReadFieldData::from_receiver(ident, field) + }) + .collect::>(); + Ok(ParselyReadStructData { + ident: value.ident, + style, + required_context: value.required_context, + alignment: value.alignment, + fields: data_fields, + }) + } +} + +impl ToTokens for ParselyReadStructData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let struct_name = &self.ident; + // Extract out the assignment expressions we'll do to assign the values of the context tuple + // to the configured variable names, as well as the types of the context tuple. + let (context_assignments, context_types) = + if let Some(ref required_context) = self.required_context { + (required_context.assignments(), required_context.types()) + } else { + (Vec::new(), Vec::new()) + }; + + let fields = &self.fields; + let field_reads = quote! { + #(#fields)* + }; + + let body = if let Some(alignment) = self.alignment { + wrap_read_with_padding_handling2( + &MemberIdent::from_ident(&self.ident), + alignment, + field_reads, + ) + } else { + field_reads + }; + let ctx_var = if context_types.is_empty() { + format_ident!("_ctx") + } else { + format_ident!("ctx") + }; + + let field_names = fields + .iter() + .map(|f| f.common.ident.as_variable_name().to_owned()) + .collect::>(); + + // TODO: reduce the duplicated code here + if self.style.is_struct() { + tokens.extend(quote! { + impl ::#crate_name::ParselyRead for #struct_name { + type Ctx = (#(#context_types,)*); + fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { + #(#context_assignments)* + + #body + + Ok(Self { #(#field_names,)* }) + } + } + }) + } else { + tokens.extend(quote! { + impl ::#crate_name::ParselyRead for #struct_name { + type Ctx = (#(#context_types,)*); + fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { + #(#context_assignments)* + + #body + + Ok(Self(#(#field_names,)* )) + } + } + }) + } + } +} diff --git a/impl/src/parsely_data/parsely_read_variant_data.rs b/impl/src/parsely_data/parsely_read_variant_data.rs new file mode 100644 index 0000000..e17e56a --- /dev/null +++ b/impl/src/parsely_data/parsely_read_variant_data.rs @@ -0,0 +1,7 @@ +use super::parsely_read_field_data::ParselyReadFieldData; + +pub struct ParselyReadVariantData { + ident: syn::Ident, + discriminant: Option, + fields: Vec, +} diff --git a/tests/expand/alignment.expanded.rs b/tests/expand/alignment.expanded.rs index 948c66b..3f42ad1 100644 --- a/tests/expand/alignment.expanded.rs +++ b/tests/expand/alignment.expanded.rs @@ -9,10 +9,10 @@ impl ::parsely_rs::ParselyRead for Foo { buf: &mut B, _ctx: (), ) -> ::parsely_rs::ParselyResult { - let __bytes_remaining_start = buf.remaining_bytes(); + let __bytes_read_before_Foo_read = buf.remaining_bytes(); let one = u8::read::(buf, ()).with_context(|| "Reading field 'one'")?; - while (__bytes_remaining_start - buf.remaining_bytes()) % 4usize != 0 { - buf.get_u8().context("padding")?; + while (__bytes_read_before_Foo_read - buf.remaining_bytes()) % 4usize != 0 { + buf.get_u8().context("consuming padding")?; } Ok(Self { one }) } From 72bf603c6c2455462ad52a7dae576382977f47e6 Mon Sep 17 00:00:00 2001 From: bbaldino Date: Sun, 25 May 2025 23:05:24 -0700 Subject: [PATCH 02/10] wip: fix collection read type --- Cargo.lock | 2 -- impl/src/parsely_data/parsely_read_field_data.rs | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 080d85d..609905b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,8 +11,6 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "bits-io" version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d4228f764442a055482d34ff0821a0d11d10d5f6972752f3208d99765b0dfbb" dependencies = [ "bitvec", "bytes", diff --git a/impl/src/parsely_data/parsely_read_field_data.rs b/impl/src/parsely_data/parsely_read_field_data.rs index 1bbf8b5..974a5c0 100644 --- a/impl/src/parsely_data/parsely_read_field_data.rs +++ b/impl/src/parsely_data/parsely_read_field_data.rs @@ -103,9 +103,10 @@ impl ToTokens for ParselyReadFieldData { } else if self.common.ty.is_collection() { // We've ensure collection_limit is set in this case elswhere. let limit = self.collection_limit.as_ref().unwrap(); + let read_type = self.buffer_type(); output.extend(generate_collection_read( limit, - &self.common.ty, + read_type, &self.context_values(), )); } else { From 0276587a1453f485663c942dd15a4c6070bb2c04 Mon Sep 17 00:00:00 2001 From: bbaldino Date: Mon, 26 May 2025 16:08:53 -0700 Subject: [PATCH 03/10] further flesh out ParselyRead handling for enums/newtype/tuple structs --- impl/src/code_gen/gen_read.rs | 176 +----------------- impl/src/lib.rs | 43 ++--- impl/src/model_types.rs | 45 +---- .../parsely_data/parsely_common_field_data.rs | 3 +- .../parsely_data/parsely_read_enum_data.rs | 116 +++++++++++- .../parsely_data/parsely_read_field_data.rs | 33 +++- .../parsely_data/parsely_read_struct_data.rs | 36 ++-- .../parsely_data/parsely_read_variant_data.rs | 73 +++++++- tests/ui/pass/enum_basic.rs | 33 ++++ tests/ui/pass/newtype_struct.rs | 11 ++ tests/ui/pass/tuple_struct.rs | 12 ++ 11 files changed, 311 insertions(+), 270 deletions(-) create mode 100644 tests/ui/pass/enum_basic.rs create mode 100644 tests/ui/pass/newtype_struct.rs create mode 100644 tests/ui/pass/tuple_struct.rs diff --git a/impl/src/code_gen/gen_read.rs b/impl/src/code_gen/gen_read.rs index dff1052..e0bb146 100644 --- a/impl/src/code_gen/gen_read.rs +++ b/impl/src/code_gen/gen_read.rs @@ -1,42 +1,29 @@ use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; use crate::{ - get_crate_name, - model_types::{wrap_read_with_padding_handling, CollectionLimit, TypedFnArgList}, + model_types::CollectionLimit, parsely_data::{ parsely_read_enum_data::ParselyReadEnumData, parsely_read_struct_data::ParselyReadStructData, }, - syn_helpers::TypeExts, - ParselyReadFieldReceiver, ParselyReadReceiver, + ParselyReadReceiver, }; pub fn generate_parsely_read_impl(data: ParselyReadReceiver) -> TokenStream { if data.data.is_struct() { let struct_data = ParselyReadStructData::try_from(data).unwrap(); - generate_parsely_read_impl_for_struct(struct_data) - // generate_parsely_read_impl_struct( - // data.ident, - // data.data.take_struct().unwrap(), - // data.alignment, - // data.required_context, - // ) + quote! { + #struct_data + } } else { - todo!() - } -} - -fn generate_parsely_read_impl_for_struct(data: ParselyReadStructData) -> TokenStream { - quote! { - #data + let enum_data = ParselyReadEnumData::try_from(data).unwrap(); + quote! { + #enum_data + } } } -fn generate_parsely_read_impl_for_enum(data: ParselyReadEnumData) -> TokenStream { - TokenStream::new() -} - pub(crate) fn generate_plain_read(ty: &syn::Type, context_values: &[syn::Expr]) -> TokenStream { quote! { #ty::read::(buf, (#(#context_values,)*)) @@ -90,146 +77,3 @@ pub(crate) fn wrap_in_optional(when_expr: &syn::Expr, inner: TokenStream) -> Tok } } } - -/// Given the data associated with a field, generate the code for properly reading it from a -/// buffer. -/// -/// The attributes set in the [`ParselyReadFieldData`] all shape the logic necessary in order to -/// properly parse this field. Roughly, the processing is as follows: -/// -/// 1. Check if an 'assign_from' attribute is set. If so, we don't read from the buffer at all and -/// instead just assign the field to the result of the given expression. -/// 2. Check if a 'map' attribute is set. If so, we'll read a value as a different type and then -/// pass it t othe map function to arrive at the final type and assign it to the field. -/// 3. Check if the field is a collection. If so, some kind of accompanying 'limit' attribute is -/// required: either a 'count' attribute or a `while_pred` attribute that defines how many -/// elements should be read. -/// 4. If none of the above are the case, do a 'plain' read where we just read the type directly -/// from the buffer. -/// 5. If an 'assertion' attribute is present then generate code to assert on the read value using -/// the given assertion function or closure. -/// 6. After the code to perform the read has been generated, we check if the field is an option -/// type. If so, a 'when' attribute is required. This is an expression that determines when the -/// read should actually be done. -/// 7. Finally, if an 'alignment' attribute is present, code is added to detect and consume any -/// padding after the read. -fn generate_field_read(field_data: &ParselyReadFieldReceiver) -> TokenStream { - let field_name = field_data - .ident - .as_ref() - .expect("Only named fields supported"); - let field_name_str = field_name.to_string(); - let read_type = field_data.buffer_type(); - // Context values that we need to pass to this field's ParselyRead::read method - let context_values = field_data.context_values(); - let mut output = TokenStream::new(); - - if let Some(ref assign_expr) = field_data.assign_from { - output.extend(quote! { - ParselyResult::<_>::Ok(#assign_expr) - }) - } else if let Some(ref map_expr) = field_data.common.map { - map_expr.to_read_map_tokens(field_name, &mut output); - } else if field_data.ty.is_collection() { - let limit = if let Some(ref count) = field_data.count { - CollectionLimit::Count(count.clone()) - } else if let Some(ref while_pred) = field_data.while_pred { - CollectionLimit::While(while_pred.clone()) - } else { - panic!("Collection field '{field_name}' must have either 'count' or 'while' attribute"); - }; - output.extend(generate_collection_read(&limit, read_type, &context_values)); - } else { - output.extend(generate_plain_read(read_type, &context_values)); - } - - if let Some(ref assertion) = field_data.common.assertion { - assertion.to_read_assertion_tokens(&field_name_str, &mut output); - } - let error_context = format!("Reading field '{field_name}'"); - output.extend(quote! { .with_context(|| #error_context)?}); - - // TODO: what cases should we allow to bypass a 'when' clause for an Option? - output = if field_data.ty.is_option() && field_data.common.map.is_none() { - let when_expr = field_data - .when - .as_ref() - .expect("Optional field '{field_name}' must have a 'when' attribute"); - wrap_in_optional(when_expr, output) - } else { - output - }; - - output = if let Some(alignment) = field_data.common.alignment { - wrap_read_with_padding_handling(field_name, alignment, output) - } else { - output - }; - - quote! { - let #field_name = #output; - } -} - -fn generate_parsely_read_impl_struct( - struct_name: syn::Ident, - fields: darling::ast::Fields, - struct_alignment: Option, - required_context: Option, -) -> TokenStream { - let crate_name = get_crate_name(); - // Extract out the assignment expressions we'll do to assign the values of the context tuple - // to the configured variable names, as well as the types of the context tuple. - let (context_assignments, context_types) = if let Some(ref required_context) = required_context - { - (required_context.assignments(), required_context.types()) - } else { - (Vec::new(), Vec::new()) - }; - - let field_reads = fields - .iter() - .map(generate_field_read) - .collect::>(); - - let field_names = fields - .iter() - .map(|f| f.ident.as_ref().unwrap()) - .collect::>(); - - let body = if let Some(alignment) = struct_alignment { - quote! { - - let __bytes_remaining_start = buf.remaining_bytes(); - - #(#field_reads)* - - while (__bytes_remaining_start - buf.remaining_bytes()) % #alignment != 0 { - buf.get_u8().context("padding")?; - } - } - } else { - quote! { - #(#field_reads)* - } - }; - - let ctx_var = if context_types.is_empty() { - format_ident!("_ctx") - } else { - format_ident!("ctx") - }; - - quote! { - impl ::#crate_name::ParselyRead for #struct_name { - type Ctx = (#(#context_types,)*); - fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { - #(#context_assignments)* - - #body - - Ok(Self { #(#field_names,)* }) - } - } - } -} diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 27181d2..247e880 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -27,7 +27,7 @@ pub mod anyhow { } use code_gen::{gen_read::generate_parsely_read_impl, gen_write::generate_parsely_write_impl}; -use darling::{ast, FromDeriveInput, FromField, FromMeta}; +use darling::{ast, FromDeriveInput, FromField, FromMeta, FromVariant}; use model_types::{Assertion, Context, ExprOrFunc, MapExpr, TypedFnArgList}; use proc_macro2::TokenStream; use syn::DeriveInput; @@ -38,6 +38,8 @@ pub fn derive_parsely_read(item: TokenStream) -> std::result::Result, } -impl ParselyReadFieldReceiver { - /// Get the 'buffer type' of this field (the type that will be used when reading from or - /// writing to the buffer): for wrapper types (like [`Option`] or [`Vec`]), this will be the - /// inner type. - /// TODO: this will move to the data type and go away from ehre - pub(crate) fn buffer_type(&self) -> &syn::Type { - if self.ty.is_option() || self.ty.is_collection() { - self.ty - .inner_type() - .expect("Option or collection has an inner type") - } else { - &self.ty - } - } - - /// Get the context values that need to be passed to the read or write call for this field - pub(crate) fn context_values(&self) -> Vec { - let field_name = self - .ident - .as_ref() - .expect("Field must have a name") - .to_string(); - if let Some(ref field_context) = self.common.context { - field_context.expressions(&format!("Read context for field '{field_name}'")) - } else { - vec![] - } - } +#[derive(Debug, FromVariant)] +#[darling(attributes(parsely, parsely_read))] +pub struct ParselyReadVariantReceiver { + ident: syn::Ident, + discriminant: Option, + id: syn::Expr, + fields: ast::Fields, } #[derive(Debug, FromField)] @@ -189,7 +170,9 @@ pub struct ParselyReadReceiver { ident: syn::Ident, required_context: Option, alignment: Option, - data: ast::Data<(), ParselyReadFieldReceiver>, + // Enums require a value to match on to determine which variant should be parsed + key: Option, + data: ast::Data, } #[derive(Debug, FromDeriveInput)] diff --git a/impl/src/model_types.rs b/impl/src/model_types.rs index bde4a91..82dc930 100644 --- a/impl/src/model_types.rs +++ b/impl/src/model_types.rs @@ -5,6 +5,7 @@ use syn::parse::Parse; use crate::get_crate_name; +#[derive(Debug)] pub(crate) enum CollectionLimit { Count(syn::Expr), While(syn::Expr), @@ -253,23 +254,7 @@ impl FromMeta for MapExpr { } impl MapExpr { - pub(crate) fn to_read_map_tokens(&self, field_name: &syn::Ident, tokens: &mut TokenStream) { - let crate_name = get_crate_name(); - let field_name_string = field_name.to_string(); - let map_expr = &self.0; - // TODO: is there a case where context might be required for reading the 'buffer_type' - // value? - tokens.extend(quote! { - { - let original_value = ::#crate_name::ParselyRead::read::(buf, ()) - .with_context(|| format!("Reading raw value for field '{}'", #field_name_string))?; - (#map_expr)(original_value).into_parsely_result() - .with_context(|| format!("Mapping raw value for field '{}'", #field_name_string)) - } - }) - } - - pub(crate) fn to_read_map_tokens2(&self, field_name: &MemberIdent, tokens: &mut TokenStream) { + pub(crate) fn to_read_map_tokens(&self, field_name: &MemberIdent, tokens: &mut TokenStream) { let crate_name = get_crate_name(); let field_name_string = field_name.as_friendly_string(); let map_expr = &self.0; @@ -350,29 +335,6 @@ impl Assertion { } pub(crate) fn wrap_read_with_padding_handling( - element_ident: &syn::Ident, - alignment: usize, - inner: TokenStream, -) -> TokenStream { - let bytes_read_before_ident = format_ident!("__bytes_read_before_{element_ident}_read"); - let bytes_read_after_ident = format_ident!("__bytes_read_after_{element_ident}_read"); - let amount_read_ident = format_ident!("__bytes_read_for_{element_ident}"); - - quote! { - let #bytes_read_before_ident = buf.remaining_bytes(); - - #inner - - let #bytes_read_after_ident = buf.remaining_bytes(); - let mut #amount_read_ident = #bytes_read_before_ident - #bytes_read_after_ident; - while #amount_read_ident % #alignment != 0 { - let _ = buf.get_u8().context("padding")?; - #amount_read_ident += 1; - } - } -} - -pub(crate) fn wrap_read_with_padding_handling2( element_ident: &MemberIdent, alignment: usize, inner: TokenStream, @@ -415,6 +377,7 @@ pub(crate) fn wrap_write_with_padding_handling( } } +#[derive(Debug)] pub(crate) enum MemberIdent { Named(syn::Ident), // Unnamed members just have an index @@ -456,6 +419,8 @@ impl MemberIdent { /// Return the value of this `MemberIdent` in the form of a `syn::Ident` such that it can be /// used to access this field inside the containing structure or enum. E.g. for a named /// variable it will be the field's name, for an unnamed variable it will be the field's index. + /// TODO: i think we'll use this for the write impls, so allow it to be dead for now + #[allow(dead_code)] pub fn field_name(&self) -> syn::Ident { match self { MemberIdent::Named(ident) => ident.clone(), diff --git a/impl/src/parsely_data/parsely_common_field_data.rs b/impl/src/parsely_data/parsely_common_field_data.rs index 6b0424e..ba845eb 100644 --- a/impl/src/parsely_data/parsely_common_field_data.rs +++ b/impl/src/parsely_data/parsely_common_field_data.rs @@ -1,6 +1,7 @@ use crate::{model_types::MemberIdent, Assertion, Context, MapExpr}; -/// Items that are common across `ParselyRead` and `ParselyWrite` fields +/// Items that are needed for both reading and writing a field to/from a buffer. +#[derive(Debug)] pub struct ParselyCommonFieldData { pub ident: MemberIdent, /// The field's type diff --git a/impl/src/parsely_data/parsely_read_enum_data.rs b/impl/src/parsely_data/parsely_read_enum_data.rs index 568c799..28f9cba 100644 --- a/impl/src/parsely_data/parsely_read_enum_data.rs +++ b/impl/src/parsely_data/parsely_read_enum_data.rs @@ -1,12 +1,116 @@ -use crate::TypedFnArgList; +use ::anyhow::anyhow; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; + +use crate::{ + anyhow, get_crate_name, + model_types::{wrap_read_with_padding_handling, MemberIdent}, + parsely_data::parsely_read_field_data::ParselyReadFieldData, + ParselyReadReceiver, TypedFnArgList, +}; use super::parsely_read_variant_data::ParselyReadVariantData; /// A struct which represents all information needed for generating a `ParselyRead` implementation /// for a given struct. -pub struct ParselyReadEnumData { - ident: syn::Ident, - required_context: Option, - alignment: Option, - variants: Vec, +#[derive(Debug)] +pub(crate) struct ParselyReadEnumData { + pub(crate) ident: syn::Ident, + pub(crate) required_context: Option, + pub(crate) alignment: Option, + pub(crate) key: syn::Expr, + pub(crate) variants: Vec, +} + +impl TryFrom for ParselyReadEnumData { + type Error = anyhow::Error; + + fn try_from(value: ParselyReadReceiver) -> Result { + let key = value + .key + .ok_or(anyhow!("'key' attribute is required on enums"))?; + let variants = value + .data + .take_enum() + .ok_or(anyhow!("Not an enum"))? + .into_iter() + .map(|v| { + let data_fields = v + .fields + .into_iter() + .enumerate() + .map(|(field_index, field)| { + let ident = MemberIdent::from_ident_or_index( + field.ident.as_ref(), + field_index as u32, + ); + ParselyReadFieldData::from_receiver(ident, field) + }) + .collect::>(); + ParselyReadVariantData { + enum_name: value.ident.clone(), + ident: v.ident, + id: v.id, + discriminant: v.discriminant, + fields: data_fields, + } + }) + .collect::>(); + + Ok(ParselyReadEnumData { + ident: value.ident, + key, + required_context: value.required_context, + alignment: value.alignment, + variants, + }) + } +} + +impl ToTokens for ParselyReadEnumData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let enum_name = &self.ident; + let (context_assignments, context_types) = + if let Some(ref required_context) = self.required_context { + (required_context.assignments(), required_context.types()) + } else { + (Vec::new(), Vec::new()) + }; + + let match_value = &self.key; + let ctx_var = if context_types.is_empty() { + format_ident!("_ctx") + } else { + format_ident!("ctx") + }; + + let match_arms = &self.variants; + let body = quote! { + match #match_value { + #(#match_arms)* + _ => ParselyResult::<_>::Err(anyhow!("No arms matched value")), + } + }; + + let body = if let Some(alignment) = self.alignment { + wrap_read_with_padding_handling(&MemberIdent::from_ident(&self.ident), alignment, body) + } else { + body + }; + + // TODO: should the enum id be able to be read from the buffer? we could have it support + // being an expr that returns a result or not, like other things. so it could be + // "buf.get_u8()" + tokens.extend(quote! { + impl ::#crate_name::ParselyRead for #enum_name { + type Ctx = (#(#context_types,)*); + fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { + #(#context_assignments)* + + #body + } + } + }); + } } diff --git a/impl/src/parsely_data/parsely_read_field_data.rs b/impl/src/parsely_data/parsely_read_field_data.rs index 974a5c0..7734c71 100644 --- a/impl/src/parsely_data/parsely_read_field_data.rs +++ b/impl/src/parsely_data/parsely_read_field_data.rs @@ -3,14 +3,15 @@ use quote::{quote, ToTokens}; use crate::{ code_gen::gen_read::{generate_collection_read, generate_plain_read, wrap_in_optional}, - model_types::{wrap_read_with_padding_handling2, CollectionLimit, MemberIdent}, + model_types::{wrap_read_with_padding_handling, CollectionLimit, MemberIdent}, ParselyReadFieldReceiver, TypeExts, }; use super::parsely_common_field_data::ParselyCommonFieldData; -/// A struct which represents all information needed for generating a `ParselyRead` implementation -/// for a given field. +/// A struct which represents all information needed for generating logic to read a field from a +/// buffer. +#[derive(Debug)] pub struct ParselyReadFieldData { /// Data common between read and write for fields pub(crate) common: ParselyCommonFieldData, @@ -92,6 +93,28 @@ impl ParselyReadFieldData { } impl ToTokens for ParselyReadFieldData { + /// Given the data associated with a field, generate the code for properly reading it from a + /// buffer. + /// + /// The attributes set in the [`ParselyReadFieldData`] all shape the logic necessary in order to + /// properly parse this field. Roughly, the processing is as follows: + /// + /// 1. Check if an 'assign_from' attribute is set. If so, we don't read from the buffer at all and + /// instead just assign the field to the result of the given expression. + /// 2. Check if a 'map' attribute is set. If so, we'll read a value as a different type and then + /// pass it t othe map function to arrive at the final type and assign it to the field. + /// 3. Check if the field is a collection. If so, some kind of accompanying 'limit' attribute is + /// required: either a 'count' attribute or a `while_pred` attribute that defines how many + /// elements should be read. + /// 4. If none of the above are the case, do a 'plain' read where we just read the type directly + /// from the buffer. + /// 5. If an 'assertion' attribute is present then generate code to assert on the read value using + /// the given assertion function or closure. + /// 6. After the code to perform the read has been generated, we check if the field is an option + /// type. If so, a 'when' attribute is required. This is an expression that determines when the + /// read should actually be done. + /// 7. Finally, if an 'alignment' attribute is present, code is added to detect and consume any + /// padding after the read. fn to_tokens(&self, tokens: &mut TokenStream) { let mut output = TokenStream::new(); if let Some(ref assign_expr) = self.assign_from { @@ -99,7 +122,7 @@ impl ToTokens for ParselyReadFieldData { ParselyResult::<_>::Ok(#assign_expr) }); } else if let Some(ref map_expr) = self.common.map { - map_expr.to_read_map_tokens2(&self.common.ident, &mut output); + map_expr.to_read_map_tokens(&self.common.ident, &mut output); } else if self.common.ty.is_collection() { // We've ensure collection_limit is set in this case elswhere. let limit = self.collection_limit.as_ref().unwrap(); @@ -134,7 +157,7 @@ impl ToTokens for ParselyReadFieldData { }; output = if let Some(alignment) = self.common.alignment { - wrap_read_with_padding_handling2(&self.common.ident, alignment, output) + wrap_read_with_padding_handling(&self.common.ident, alignment, output) } else { output }; diff --git a/impl/src/parsely_data/parsely_read_struct_data.rs b/impl/src/parsely_data/parsely_read_struct_data.rs index 7ad27f6..9f9e29d 100644 --- a/impl/src/parsely_data/parsely_read_struct_data.rs +++ b/impl/src/parsely_data/parsely_read_struct_data.rs @@ -4,7 +4,7 @@ use quote::{format_ident, quote, ToTokens}; use crate::{ get_crate_name, - model_types::{wrap_read_with_padding_handling2, MemberIdent}, + model_types::{wrap_read_with_padding_handling, MemberIdent}, ParselyReadReceiver, TypedFnArgList, }; @@ -67,7 +67,7 @@ impl ToTokens for ParselyReadStructData { }; let body = if let Some(alignment) = self.alignment { - wrap_read_with_padding_handling2( + wrap_read_with_padding_handling( &MemberIdent::from_ident(&self.ident), alignment, field_reads, @@ -89,30 +89,30 @@ impl ToTokens for ParselyReadStructData { // TODO: reduce the duplicated code here if self.style.is_struct() { tokens.extend(quote! { - impl ::#crate_name::ParselyRead for #struct_name { - type Ctx = (#(#context_types,)*); - fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { - #(#context_assignments)* + impl ::#crate_name::ParselyRead for #struct_name { + type Ctx = (#(#context_types,)*); + fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { + #(#context_assignments)* - #body + #body - Ok(Self { #(#field_names,)* }) + Ok(Self { #(#field_names,)* }) + } } - } - }) + }) } else { tokens.extend(quote! { - impl ::#crate_name::ParselyRead for #struct_name { - type Ctx = (#(#context_types,)*); - fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { - #(#context_assignments)* + impl ::#crate_name::ParselyRead for #struct_name { + type Ctx = (#(#context_types,)*); + fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { + #(#context_assignments)* - #body + #body - Ok(Self(#(#field_names,)* )) + Ok(Self(#(#field_names,)* )) + } } - } - }) + }) } } } diff --git a/impl/src/parsely_data/parsely_read_variant_data.rs b/impl/src/parsely_data/parsely_read_variant_data.rs index e17e56a..ba80cb4 100644 --- a/impl/src/parsely_data/parsely_read_variant_data.rs +++ b/impl/src/parsely_data/parsely_read_variant_data.rs @@ -1,7 +1,72 @@ +use crate::model_types::MemberIdent; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + use super::parsely_read_field_data::ParselyReadFieldData; -pub struct ParselyReadVariantData { - ident: syn::Ident, - discriminant: Option, - fields: Vec, +#[derive(Debug)] +pub(crate) struct ParselyReadVariantData { + pub(crate) enum_name: syn::Ident, + pub(crate) ident: syn::Ident, + pub(crate) id: syn::Expr, + pub(crate) discriminant: Option, + pub(crate) fields: Vec, +} + +impl ParselyReadVariantData { + fn named_fields(&self) -> bool { + self.fields + .iter() + .any(|f| matches!(f.common.ident, MemberIdent::Named(_))) + } +} + +impl ToTokens for ParselyReadVariantData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let arm_expr = &self.id; + let arm_body = if let Some(ref discriminant) = self.discriminant { + quote! { + #discriminant + } + } else { + let fields = &self.fields; + quote! { + #(#fields)* + + + } + }; + let field_names = self + .fields + .iter() + .map(|f| f.common.ident.as_variable_name().to_owned()) + .collect::>(); + let enum_name = &self.enum_name; + let variant_name = &self.ident; + if self.fields.is_empty() { + tokens.extend(quote! { + #arm_expr => { + #arm_body + + Ok(#enum_name::#variant_name) + } + }) + } else if self.named_fields() { + tokens.extend(quote! { + #arm_expr => { + #arm_body + + Ok(#enum_name::#variant_name { #(#field_names,)* }) + } + }) + } else { + tokens.extend(quote! { + #arm_expr => { + #arm_body + + Ok(#enum_name::#variant_name(#(#field_names,)* )) + } + }) + } + } } diff --git a/tests/ui/pass/enum_basic.rs b/tests/ui/pass/enum_basic.rs new file mode 100644 index 0000000..7189306 --- /dev/null +++ b/tests/ui/pass/enum_basic.rs @@ -0,0 +1,33 @@ +use parsely_rs::*; + +#[derive(Debug, ParselyRead)] +#[parsely_read(key = "buf.get_u8().unwrap()")] +enum Foo { + #[parsely_read(id = 1)] + One, + #[parsely_read(id = 2)] + Two(u8), + #[parsely_read(id = 3)] + Three { bar: u8, baz: u16 }, +} + +fn main() { + #[rustfmt::skip] + let mut bits = Bits::from_static_bytes( + &[ + // First instance: Foo::One, no data + 1, + // Second instance: Foo::Two, value of 1 + 2, 1, + // Third instance: Foo::Three, { bar: 1, baz: 42 } + 3, 1, 0, 42, + ] + ); + + let one = Foo::read::(&mut bits, ()).expect("one"); + assert!(matches!(one, Foo::One)); + let two = Foo::read::(&mut bits, ()).expect("two"); + assert!(matches!(two, Foo::Two(1))); + let three = Foo::read::(&mut bits, ()).expect("three"); + assert!(matches!(three, Foo::Three { bar: 1, baz: 42 })); +} diff --git a/tests/ui/pass/newtype_struct.rs b/tests/ui/pass/newtype_struct.rs new file mode 100644 index 0000000..7f6af31 --- /dev/null +++ b/tests/ui/pass/newtype_struct.rs @@ -0,0 +1,11 @@ +use parsely_rs::*; + +#[derive(ParselyRead)] +struct Foo(u8); + +fn main() { + let mut bits = Bits::from_static_bytes(&[42]); + + let foo = Foo::read::(&mut bits, ()).expect("successful parse"); + assert_eq!(foo.0, 42); +} diff --git a/tests/ui/pass/tuple_struct.rs b/tests/ui/pass/tuple_struct.rs new file mode 100644 index 0000000..5f69bc0 --- /dev/null +++ b/tests/ui/pass/tuple_struct.rs @@ -0,0 +1,12 @@ +use parsely_rs::*; + +#[derive(ParselyRead)] +struct Foo(u8, u8); + +fn main() { + let mut bits = Bits::from_static_bytes(&[42, 43]); + + let foo = Foo::read::(&mut bits, ()).expect("successful parse"); + assert_eq!(foo.0, 42); + assert_eq!(foo.1, 43); +} From 4e9626a0102e9b14f37da6b14bdf00b2a8b6f7cf Mon Sep 17 00:00:00 2001 From: bbaldino Date: Mon, 26 May 2025 22:16:20 -0700 Subject: [PATCH 04/10] code reorganization --- Notes.md | 75 +++++++++++++++++++ impl/src/code_gen/gen_read.rs | 2 +- impl/src/parsely_data/mod.rs | 7 +- .../parsely_data/parsely_common_field_data.rs | 14 ++-- impl/src/parsely_data/read/mod.rs | 4 + .../{ => read}/parsely_read_enum_data.rs | 6 +- .../{ => read}/parsely_read_field_data.rs | 3 +- .../{ => read}/parsely_read_struct_data.rs | 0 .../{ => read}/parsely_read_variant_data.rs | 0 9 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 impl/src/parsely_data/read/mod.rs rename impl/src/parsely_data/{ => read}/parsely_read_enum_data.rs (96%) rename impl/src/parsely_data/{ => read}/parsely_read_field_data.rs (99%) rename impl/src/parsely_data/{ => read}/parsely_read_struct_data.rs (100%) rename impl/src/parsely_data/{ => read}/parsely_read_variant_data.rs (100%) diff --git a/Notes.md b/Notes.md index dd53fd2..9e789b3 100644 --- a/Notes.md +++ b/Notes.md @@ -506,3 +506,78 @@ able to provide different/optimized read/write impls for different types (e.g. one for BitBuf and another when we know it's a Bits specifically or something) --> look into a refactoring of the read/write traits to accomplish this + +--> Once I cleaned up the design of BitBuf/BitBufMut/Bits/BitsMut, I found I +didn't end up really needing this. So although it was kinda a neat trick, it +also added a lot of complexity (and is really verbose when you wanted to use a +different buffer type, which was annoying) so I ended up ripping this out. + +### Order of operations for different attributes + +As of this writing, for a ParselyRead impl the following attributes are supported: + +* Assertion (assert on a read value) +* Map (map a read value) +* Alignment (consuming data after a read to reach the given alignment) +* Context (pass values when calling a field's read method) +* Limit (denote how many values a collection field contains) +* Assignment (assign a field to a value instead of reading it) +* When (denote when an optional field should be read) + +Some of these attributes are pretty orthogonal to the others (alignment, +context) but others could potentially "interact" in different ways depending on +some desired order. For example: if 'map' and 'assertion' are present, should +the assertion be performed on the read value before or after the map function +was applied? There is no right answer to that question, of course: a caller +could want it either way. So the question is how to implement support for +that. Probably the order of the attributes should be used to determine the +order things are applied: + +This example should: + +1. Read the value +1. Run the assertion on the 'raw' value +1. Run the mapping function + +```rust +#[derive(ParselyRead)] +struct Foo { + #[parsely_read(assertion = "|v: &u8| v % 2 == 0", map = "|v: u8| v * 2")] + field_one: u8 +} +``` + +This example should: + +1. Read the value +1. Run the mapping function +1. Run the assertion on the mapped value + +```rust +#[derive(ParselyRead)] +struct Foo { + #[parsely_read(map = "|v: u8| v * 2", assertion = "|v: &u8| v % 2 == 0")] + field_one: u8 +} +``` + +Order-sensitive interactions of attributes: +Assertion/Map - assertion run on raw value or mapped value +Map/Limit - map function run on individual items or entire collection? Not sure +both are really needed. + +Given the limited number of interactions, I think for now I'll do an implicit, +hard-coded order-of-operations: + +1. When - In a way this has the highest precedence because it determines if + _any_ of the read code for this field is run at all +1. Assignment +1. Limit - For collection fields this determines how many times the 'core' read + is run. +1. (Context - This is really orthogonal so don't think it has much effect + either way) +1. Assertion - this is the first thing done after a value has been read from + the buffer. It's always done on the 'raw' (un-mapped) value. +1. Map +1. (Alignment - this is 'outside' the scope of a field's specific read, so is + also pretty orthogonal) diff --git a/impl/src/code_gen/gen_read.rs b/impl/src/code_gen/gen_read.rs index e0bb146..26d5eea 100644 --- a/impl/src/code_gen/gen_read.rs +++ b/impl/src/code_gen/gen_read.rs @@ -3,7 +3,7 @@ use quote::quote; use crate::{ model_types::CollectionLimit, - parsely_data::{ + parsely_data::read::{ parsely_read_enum_data::ParselyReadEnumData, parsely_read_struct_data::ParselyReadStructData, }, diff --git a/impl/src/parsely_data/mod.rs b/impl/src/parsely_data/mod.rs index 68aeec1..bb0e238 100644 --- a/impl/src/parsely_data/mod.rs +++ b/impl/src/parsely_data/mod.rs @@ -1,5 +1,2 @@ -mod parsely_common_field_data; -pub mod parsely_read_enum_data; -pub mod parsely_read_field_data; -pub mod parsely_read_struct_data; -pub mod parsely_read_variant_data; +pub mod parsely_common_field_data; +pub mod read; diff --git a/impl/src/parsely_data/parsely_common_field_data.rs b/impl/src/parsely_data/parsely_common_field_data.rs index ba845eb..5fd560f 100644 --- a/impl/src/parsely_data/parsely_common_field_data.rs +++ b/impl/src/parsely_data/parsely_common_field_data.rs @@ -2,18 +2,18 @@ use crate::{model_types::MemberIdent, Assertion, Context, MapExpr}; /// Items that are needed for both reading and writing a field to/from a buffer. #[derive(Debug)] -pub struct ParselyCommonFieldData { - pub ident: MemberIdent, +pub(crate) struct ParselyCommonFieldData { + pub(crate) ident: MemberIdent, /// The field's type - pub ty: syn::Type, + pub(crate) ty: syn::Type, - pub assertion: Option, + pub(crate) assertion: Option, /// Values that need to be passed as context to this fields read or write method - pub context: Option, + pub(crate) context: Option, /// An optional mapping that will be applied to the read value - pub map: Option, + pub(crate) map: Option, /// An optional indicator that this field is or needs to be aligned to the given byte alignment /// via padding. - pub alignment: Option, + pub(crate) alignment: Option, } diff --git a/impl/src/parsely_data/read/mod.rs b/impl/src/parsely_data/read/mod.rs new file mode 100644 index 0000000..0f81e55 --- /dev/null +++ b/impl/src/parsely_data/read/mod.rs @@ -0,0 +1,4 @@ +pub mod parsely_read_enum_data; +pub mod parsely_read_field_data; +pub mod parsely_read_struct_data; +pub mod parsely_read_variant_data; diff --git a/impl/src/parsely_data/parsely_read_enum_data.rs b/impl/src/parsely_data/read/parsely_read_enum_data.rs similarity index 96% rename from impl/src/parsely_data/parsely_read_enum_data.rs rename to impl/src/parsely_data/read/parsely_read_enum_data.rs index 28f9cba..cc94e92 100644 --- a/impl/src/parsely_data/parsely_read_enum_data.rs +++ b/impl/src/parsely_data/read/parsely_read_enum_data.rs @@ -5,11 +5,13 @@ use quote::{format_ident, quote, ToTokens}; use crate::{ anyhow, get_crate_name, model_types::{wrap_read_with_padding_handling, MemberIdent}, - parsely_data::parsely_read_field_data::ParselyReadFieldData, ParselyReadReceiver, TypedFnArgList, }; -use super::parsely_read_variant_data::ParselyReadVariantData; +use super::{ + parsely_read_field_data::ParselyReadFieldData, + parsely_read_variant_data::ParselyReadVariantData, +}; /// A struct which represents all information needed for generating a `ParselyRead` implementation /// for a given struct. diff --git a/impl/src/parsely_data/parsely_read_field_data.rs b/impl/src/parsely_data/read/parsely_read_field_data.rs similarity index 99% rename from impl/src/parsely_data/parsely_read_field_data.rs rename to impl/src/parsely_data/read/parsely_read_field_data.rs index 7734c71..5661b7c 100644 --- a/impl/src/parsely_data/parsely_read_field_data.rs +++ b/impl/src/parsely_data/read/parsely_read_field_data.rs @@ -4,11 +4,10 @@ use quote::{quote, ToTokens}; use crate::{ code_gen::gen_read::{generate_collection_read, generate_plain_read, wrap_in_optional}, model_types::{wrap_read_with_padding_handling, CollectionLimit, MemberIdent}, + parsely_data::parsely_common_field_data::ParselyCommonFieldData, ParselyReadFieldReceiver, TypeExts, }; -use super::parsely_common_field_data::ParselyCommonFieldData; - /// A struct which represents all information needed for generating logic to read a field from a /// buffer. #[derive(Debug)] diff --git a/impl/src/parsely_data/parsely_read_struct_data.rs b/impl/src/parsely_data/read/parsely_read_struct_data.rs similarity index 100% rename from impl/src/parsely_data/parsely_read_struct_data.rs rename to impl/src/parsely_data/read/parsely_read_struct_data.rs diff --git a/impl/src/parsely_data/parsely_read_variant_data.rs b/impl/src/parsely_data/read/parsely_read_variant_data.rs similarity index 100% rename from impl/src/parsely_data/parsely_read_variant_data.rs rename to impl/src/parsely_data/read/parsely_read_variant_data.rs From a972887c2ac0ef2ca834d3b6d199fdc4eb66de2e Mon Sep 17 00:00:00 2001 From: bbaldino Date: Mon, 26 May 2025 22:23:54 -0700 Subject: [PATCH 05/10] more code reorganization --- impl/src/code_gen/helpers.rs | 12 +++++++ impl/src/code_gen/mod.rs | 4 ++- .../parsely_common_field_data.rs | 0 .../code_gen/{gen_read.rs => read/helpers.rs} | 33 +------------------ .../{parsely_data => code_gen}/read/mod.rs | 1 + .../read/parsely_read_enum_data.rs | 0 .../read/parsely_read_field_data.rs | 5 +-- .../read/parsely_read_struct_data.rs | 0 .../read/parsely_read_variant_data.rs | 0 impl/src/lib.rs | 22 +++++++++++-- impl/src/parsely_data/mod.rs | 2 -- 11 files changed, 39 insertions(+), 40 deletions(-) create mode 100644 impl/src/code_gen/helpers.rs rename impl/src/{parsely_data => code_gen}/parsely_common_field_data.rs (100%) rename impl/src/code_gen/{gen_read.rs => read/helpers.rs} (65%) rename impl/src/{parsely_data => code_gen}/read/mod.rs (88%) rename impl/src/{parsely_data => code_gen}/read/parsely_read_enum_data.rs (100%) rename impl/src/{parsely_data => code_gen}/read/parsely_read_field_data.rs (97%) rename impl/src/{parsely_data => code_gen}/read/parsely_read_struct_data.rs (100%) rename impl/src/{parsely_data => code_gen}/read/parsely_read_variant_data.rs (100%) delete mode 100644 impl/src/parsely_data/mod.rs diff --git a/impl/src/code_gen/helpers.rs b/impl/src/code_gen/helpers.rs new file mode 100644 index 0000000..3d02fcb --- /dev/null +++ b/impl/src/code_gen/helpers.rs @@ -0,0 +1,12 @@ +use proc_macro2::TokenStream; +use quote::quote; + +pub(crate) fn wrap_in_optional(condition: &syn::Expr, inner: TokenStream) -> TokenStream { + quote! { + if #condition { + Some(#inner) + } else { + None + } + } +} diff --git a/impl/src/code_gen/mod.rs b/impl/src/code_gen/mod.rs index a0f0993..c2467d7 100644 --- a/impl/src/code_gen/mod.rs +++ b/impl/src/code_gen/mod.rs @@ -1,2 +1,4 @@ -pub(crate) mod gen_read; pub(crate) mod gen_write; +pub(crate) mod helpers; +pub(crate) mod parsely_common_field_data; +pub(crate) mod read; diff --git a/impl/src/parsely_data/parsely_common_field_data.rs b/impl/src/code_gen/parsely_common_field_data.rs similarity index 100% rename from impl/src/parsely_data/parsely_common_field_data.rs rename to impl/src/code_gen/parsely_common_field_data.rs diff --git a/impl/src/code_gen/gen_read.rs b/impl/src/code_gen/read/helpers.rs similarity index 65% rename from impl/src/code_gen/gen_read.rs rename to impl/src/code_gen/read/helpers.rs index 26d5eea..257e153 100644 --- a/impl/src/code_gen/gen_read.rs +++ b/impl/src/code_gen/read/helpers.rs @@ -1,28 +1,7 @@ use proc_macro2::TokenStream; use quote::quote; -use crate::{ - model_types::CollectionLimit, - parsely_data::read::{ - parsely_read_enum_data::ParselyReadEnumData, - parsely_read_struct_data::ParselyReadStructData, - }, - ParselyReadReceiver, -}; - -pub fn generate_parsely_read_impl(data: ParselyReadReceiver) -> TokenStream { - if data.data.is_struct() { - let struct_data = ParselyReadStructData::try_from(data).unwrap(); - quote! { - #struct_data - } - } else { - let enum_data = ParselyReadEnumData::try_from(data).unwrap(); - quote! { - #enum_data - } - } -} +use crate::model_types::CollectionLimit; pub(crate) fn generate_plain_read(ty: &syn::Type, context_values: &[syn::Expr]) -> TokenStream { quote! { @@ -67,13 +46,3 @@ pub(crate) fn generate_collection_read( } } } - -pub(crate) fn wrap_in_optional(when_expr: &syn::Expr, inner: TokenStream) -> TokenStream { - quote! { - if #when_expr { - Some(#inner) - } else { - None - } - } -} diff --git a/impl/src/parsely_data/read/mod.rs b/impl/src/code_gen/read/mod.rs similarity index 88% rename from impl/src/parsely_data/read/mod.rs rename to impl/src/code_gen/read/mod.rs index 0f81e55..b3f1cb6 100644 --- a/impl/src/parsely_data/read/mod.rs +++ b/impl/src/code_gen/read/mod.rs @@ -1,3 +1,4 @@ +pub mod helpers; pub mod parsely_read_enum_data; pub mod parsely_read_field_data; pub mod parsely_read_struct_data; diff --git a/impl/src/parsely_data/read/parsely_read_enum_data.rs b/impl/src/code_gen/read/parsely_read_enum_data.rs similarity index 100% rename from impl/src/parsely_data/read/parsely_read_enum_data.rs rename to impl/src/code_gen/read/parsely_read_enum_data.rs diff --git a/impl/src/parsely_data/read/parsely_read_field_data.rs b/impl/src/code_gen/read/parsely_read_field_data.rs similarity index 97% rename from impl/src/parsely_data/read/parsely_read_field_data.rs rename to impl/src/code_gen/read/parsely_read_field_data.rs index 5661b7c..4a67b78 100644 --- a/impl/src/parsely_data/read/parsely_read_field_data.rs +++ b/impl/src/code_gen/read/parsely_read_field_data.rs @@ -2,12 +2,13 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use crate::{ - code_gen::gen_read::{generate_collection_read, generate_plain_read, wrap_in_optional}, + code_gen::{helpers::wrap_in_optional, parsely_common_field_data::ParselyCommonFieldData}, model_types::{wrap_read_with_padding_handling, CollectionLimit, MemberIdent}, - parsely_data::parsely_common_field_data::ParselyCommonFieldData, ParselyReadFieldReceiver, TypeExts, }; +use super::helpers::{generate_collection_read, generate_plain_read}; + /// A struct which represents all information needed for generating logic to read a field from a /// buffer. #[derive(Debug)] diff --git a/impl/src/parsely_data/read/parsely_read_struct_data.rs b/impl/src/code_gen/read/parsely_read_struct_data.rs similarity index 100% rename from impl/src/parsely_data/read/parsely_read_struct_data.rs rename to impl/src/code_gen/read/parsely_read_struct_data.rs diff --git a/impl/src/parsely_data/read/parsely_read_variant_data.rs b/impl/src/code_gen/read/parsely_read_variant_data.rs similarity index 100% rename from impl/src/parsely_data/read/parsely_read_variant_data.rs rename to impl/src/code_gen/read/parsely_read_variant_data.rs diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 247e880..cb5ccda 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -1,7 +1,6 @@ mod code_gen; pub mod error; mod model_types; -pub mod parsely_data; pub mod parsely_read; pub mod parsely_write; mod syn_helpers; @@ -26,10 +25,17 @@ pub mod anyhow { pub use anyhow::*; } -use code_gen::{gen_read::generate_parsely_read_impl, gen_write::generate_parsely_write_impl}; +use code_gen::{ + gen_write::generate_parsely_write_impl, + read::{ + parsely_read_enum_data::ParselyReadEnumData, + parsely_read_struct_data::ParselyReadStructData, + }, +}; use darling::{ast, FromDeriveInput, FromField, FromMeta, FromVariant}; use model_types::{Assertion, Context, ExprOrFunc, MapExpr, TypedFnArgList}; use proc_macro2::TokenStream; +use quote::quote; use syn::DeriveInput; use syn_helpers::TypeExts; @@ -40,7 +46,17 @@ pub fn derive_parsely_read(item: TokenStream) -> std::result::Result Date: Thu, 29 May 2025 11:05:52 -0700 Subject: [PATCH 06/10] refactor: change the write code generation --- impl/src/code_gen/gen_write.rs | 167 ------------------ impl/src/code_gen/mod.rs | 3 +- .../src/code_gen/parsely_common_field_data.rs | 29 ++- impl/src/code_gen/read/helpers.rs | 24 ++- .../code_gen/read/parsely_read_enum_data.rs | 6 +- .../code_gen/read/parsely_read_field_data.rs | 40 +---- .../code_gen/read/parsely_read_struct_data.rs | 10 +- impl/src/code_gen/write/helpers.rs | 25 +++ impl/src/code_gen/write/mod.rs | 3 + .../write/parsely_write_field_data.rs | 116 ++++++++++++ .../write/parsely_write_struct_data.rs | 112 ++++++++++++ impl/src/lib.rs | 61 ++----- impl/src/model_types.rs | 45 ----- tests/expand/alignment.expanded.rs | 8 +- 14 files changed, 342 insertions(+), 307 deletions(-) delete mode 100644 impl/src/code_gen/gen_write.rs create mode 100644 impl/src/code_gen/write/helpers.rs create mode 100644 impl/src/code_gen/write/mod.rs create mode 100644 impl/src/code_gen/write/parsely_write_field_data.rs create mode 100644 impl/src/code_gen/write/parsely_write_struct_data.rs diff --git a/impl/src/code_gen/gen_write.rs b/impl/src/code_gen/gen_write.rs deleted file mode 100644 index eff0cc2..0000000 --- a/impl/src/code_gen/gen_write.rs +++ /dev/null @@ -1,167 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; - -use crate::{ - get_crate_name, - model_types::{wrap_write_with_padding_handling, TypedFnArgList}, - syn_helpers::TypeExts, - ParselyWriteData, ParselyWriteFieldData, -}; - -pub fn generate_parsely_write_impl(data: ParselyWriteData) -> TokenStream { - let struct_name = data.ident; - if data.data.is_struct() { - generate_parsely_write_impl_struct( - struct_name, - data.data.take_struct().unwrap(), - data.required_context, - data.alignment, - data.sync_args, - ) - } else { - todo!() - } -} - -fn generate_parsely_write_impl_struct( - struct_name: syn::Ident, - fields: darling::ast::Fields, - required_context: Option, - struct_alignment: Option, - sync_args: Option, -) -> TokenStream { - let crate_name = get_crate_name(); - let (context_assignments, context_types) = if let Some(ref required_context) = required_context - { - (required_context.assignments(), required_context.types()) - } else { - (Vec::new(), Vec::new()) - }; - // TODO: clean this up like the read code - let field_writes = fields - .iter() - .map(|f| { - let field_name = f.ident.as_ref().expect("Field has a name"); - let field_name_string = field_name.to_string(); - let write_type = f.buffer_type(); - // Context values that we need to pass to this field's ParselyWrite::write method - let context_values = f.context_values(); - - let mut field_write_output = TokenStream::new(); - - if let Some(ref assertion) = f.common.assertion { - assertion.to_write_assertion_tokens(&field_name_string, &mut field_write_output); - } - - // TODO: these write calls should be qualified. Something like <#write_type as - // ParselyWrite>::write - if let Some(ref map_expr) = f.common.map { - map_expr.to_write_map_tokens(field_name, &mut field_write_output); - } else if f.ty.is_option() { - field_write_output.extend(quote! { - if let Some(ref v) = self.#field_name { - #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; - } - }); - } else if f.ty.is_collection() { - field_write_output.extend(quote! { - self.#field_name.iter().enumerate().map(|(idx, v)| { - #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Index {idx}")) - }).collect::>>().with_context(|| format!("Writing field '{}'", #field_name_string))?; - }); - } else { - field_write_output.extend(quote! { - #write_type::write::(&self.#field_name, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; - }); - } - - field_write_output = if let Some(alignment) = f.common.alignment { - wrap_write_with_padding_handling(field_name, alignment, field_write_output) - } else { - field_write_output - }; - - field_write_output - }) - .collect::>(); - - // sync_args_types lays out the types inside this struct's sync method args tuple. Unless a - // 'sync_args' attribute was used, by default it doesn't need any. - let (sync_args_variables, sync_args_types) = if let Some(ref sync_args) = sync_args { - (sync_args.names(), sync_args.types()) - } else { - (Vec::new(), Vec::new()) - }; - - let sync_field_calls = fields - .iter() - // 'sync_with' attirbutes mean this field's 'sync' method needs to be called with some data - // Iterate over all fields and either: - // a) execute the expression provided in the sync_expr attribute - // b) call the sync function on that type with any provided sync_with arguments - .map(|f| { - let field_name = f.ident.as_ref().expect("Field has a name"); - let field_name_string = field_name.to_string(); - if let Some(ref sync_expr) = f.sync_expr { - quote! { - self.#field_name = (#sync_expr).into_parsely_result().with_context(|| format!("Syncing field '{}'", #field_name_string))?; - } - } else if f.sync_with.is_empty() && f.ty.is_wrapped() { - // We'll allow this combination to skip a call to sync: for types like Option or - // Vec, synchronization is only going to make sense if a custom function was - // provided. - quote! {} - } else { - let sync_with = f.sync_with_expressions(); - quote! { - self.#field_name.sync((#(#sync_with,)*)).with_context(|| format!("Syncing field '{}'", #field_name_string))?; - } - } - }) - .collect::>(); - - let body = if let Some(alignment) = struct_alignment { - quote! { - - let __bytes_remaining_start = buf.remaining_mut_bytes(); - - #(#field_writes)* - - while (__bytes_remaining_start - buf.remaining_mut_bytes()) % #alignment != 0 { - let _ = buf.put_u8(0).context("padding")?; - } - - } - } else { - quote! { - #(#field_writes)* - } - }; - - quote! { - impl ::#crate_name::ParselyWrite for #struct_name { - type Ctx = (#(#context_types,)*); - fn write( - &self, - buf: &mut B, - ctx: Self::Ctx, - ) -> ParselyResult<()> { - #(#context_assignments)* - - #body - - Ok(()) - } - } - - impl StateSync for #struct_name { - type SyncCtx = (#(#sync_args_types,)*); - fn sync(&mut self, (#(#sync_args_variables,)*): (#(#sync_args_types,)*)) -> ParselyResult<()> { - #(#sync_field_calls)* - - Ok(()) - } - - } - } -} diff --git a/impl/src/code_gen/mod.rs b/impl/src/code_gen/mod.rs index c2467d7..c2e73f2 100644 --- a/impl/src/code_gen/mod.rs +++ b/impl/src/code_gen/mod.rs @@ -1,4 +1,5 @@ -pub(crate) mod gen_write; +// pub(crate) mod gen_write; pub(crate) mod helpers; pub(crate) mod parsely_common_field_data; pub(crate) mod read; +pub(crate) mod write; diff --git a/impl/src/code_gen/parsely_common_field_data.rs b/impl/src/code_gen/parsely_common_field_data.rs index 5fd560f..9be7210 100644 --- a/impl/src/code_gen/parsely_common_field_data.rs +++ b/impl/src/code_gen/parsely_common_field_data.rs @@ -1,4 +1,4 @@ -use crate::{model_types::MemberIdent, Assertion, Context, MapExpr}; +use crate::{model_types::MemberIdent, syn_helpers::TypeExts, Assertion, Context, MapExpr}; /// Items that are needed for both reading and writing a field to/from a buffer. #[derive(Debug)] @@ -17,3 +17,30 @@ pub(crate) struct ParselyCommonFieldData { /// via padding. pub(crate) alignment: Option, } + +impl ParselyCommonFieldData { + /// Get the 'buffer type' of this field (the type that will be used when reading from or + /// writing to the buffer): for wrapper types (like [`Option`] or [`Vec`]), this will be the + /// inner type. + pub(crate) fn buffer_type(&self) -> &syn::Type { + if self.ty.is_option() || self.ty.is_collection() { + self.ty + .inner_type() + .expect("Option or collection has an inner type") + } else { + &self.ty + } + } + + /// Get the context values that need to be passed to the read or write call for this field + pub(crate) fn context_values(&self) -> Vec { + if let Some(ref field_context) = self.context { + field_context.expressions(&format!( + "Read context for field '{}'", + self.ident.as_friendly_string() + )) + } else { + vec![] + } + } +} diff --git a/impl/src/code_gen/read/helpers.rs b/impl/src/code_gen/read/helpers.rs index 257e153..ce25815 100644 --- a/impl/src/code_gen/read/helpers.rs +++ b/impl/src/code_gen/read/helpers.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; -use crate::model_types::CollectionLimit; +use crate::model_types::{CollectionLimit, MemberIdent}; pub(crate) fn generate_plain_read(ty: &syn::Type, context_values: &[syn::Expr]) -> TokenStream { quote! { @@ -46,3 +46,23 @@ pub(crate) fn generate_collection_read( } } } + +pub(crate) fn wrap_read_with_padding_handling( + element_ident: &MemberIdent, + alignment: usize, + inner: TokenStream, +) -> TokenStream { + let bytes_read_before_ident = format_ident!( + "__bytes_read_before_{}_read", + element_ident.as_friendly_string() + ); + quote! { + let #bytes_read_before_ident = buf.remaining_bytes(); + + #inner + + while (#bytes_read_before_ident - buf.remaining_bytes()) % #alignment != 0 { + buf.get_u8().context("consuming padding")?; + } + } +} diff --git a/impl/src/code_gen/read/parsely_read_enum_data.rs b/impl/src/code_gen/read/parsely_read_enum_data.rs index cc94e92..e1121e9 100644 --- a/impl/src/code_gen/read/parsely_read_enum_data.rs +++ b/impl/src/code_gen/read/parsely_read_enum_data.rs @@ -3,13 +3,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use crate::{ - anyhow, get_crate_name, - model_types::{wrap_read_with_padding_handling, MemberIdent}, - ParselyReadReceiver, TypedFnArgList, + anyhow, get_crate_name, model_types::MemberIdent, ParselyReadReceiver, TypedFnArgList, }; use super::{ - parsely_read_field_data::ParselyReadFieldData, + helpers::wrap_read_with_padding_handling, parsely_read_field_data::ParselyReadFieldData, parsely_read_variant_data::ParselyReadVariantData, }; diff --git a/impl/src/code_gen/read/parsely_read_field_data.rs b/impl/src/code_gen/read/parsely_read_field_data.rs index 4a67b78..d152e56 100644 --- a/impl/src/code_gen/read/parsely_read_field_data.rs +++ b/impl/src/code_gen/read/parsely_read_field_data.rs @@ -3,11 +3,13 @@ use quote::{quote, ToTokens}; use crate::{ code_gen::{helpers::wrap_in_optional, parsely_common_field_data::ParselyCommonFieldData}, - model_types::{wrap_read_with_padding_handling, CollectionLimit, MemberIdent}, + model_types::{CollectionLimit, MemberIdent}, ParselyReadFieldReceiver, TypeExts, }; -use super::helpers::{generate_collection_read, generate_plain_read}; +use super::helpers::{ + generate_collection_read, generate_plain_read, wrap_read_with_padding_handling, +}; /// A struct which represents all information needed for generating logic to read a field from a /// buffer. @@ -64,32 +66,6 @@ impl ParselyReadFieldData { when, } } - - /// Get the 'buffer type' of this field (the type that will be used when reading from or - /// writing to the buffer): for wrapper types (like [`Option`] or [`Vec`]), this will be the - /// inner type. - pub(crate) fn buffer_type(&self) -> &syn::Type { - if self.common.ty.is_option() || self.common.ty.is_collection() { - self.common - .ty - .inner_type() - .expect("Option or collection has an inner type") - } else { - &self.common.ty - } - } - - /// Get the context values that need to be passed to the read or write call for this field - pub(crate) fn context_values(&self) -> Vec { - if let Some(ref field_context) = self.common.context { - field_context.expressions(&format!( - "Read context for field '{}'", - self.common.ident.as_friendly_string() - )) - } else { - vec![] - } - } } impl ToTokens for ParselyReadFieldData { @@ -126,16 +102,16 @@ impl ToTokens for ParselyReadFieldData { } else if self.common.ty.is_collection() { // We've ensure collection_limit is set in this case elswhere. let limit = self.collection_limit.as_ref().unwrap(); - let read_type = self.buffer_type(); + let read_type = self.common.buffer_type(); output.extend(generate_collection_read( limit, read_type, - &self.context_values(), + &self.common.context_values(), )); } else { output.extend(generate_plain_read( - self.buffer_type(), - &self.context_values(), + self.common.buffer_type(), + &self.common.context_values(), )); } diff --git a/impl/src/code_gen/read/parsely_read_struct_data.rs b/impl/src/code_gen/read/parsely_read_struct_data.rs index 9f9e29d..40e33d4 100644 --- a/impl/src/code_gen/read/parsely_read_struct_data.rs +++ b/impl/src/code_gen/read/parsely_read_struct_data.rs @@ -2,13 +2,11 @@ use anyhow::anyhow; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; -use crate::{ - get_crate_name, - model_types::{wrap_read_with_padding_handling, MemberIdent}, - ParselyReadReceiver, TypedFnArgList, -}; +use crate::{get_crate_name, model_types::MemberIdent, ParselyReadReceiver, TypedFnArgList}; -use super::parsely_read_field_data::ParselyReadFieldData; +use super::{ + helpers::wrap_read_with_padding_handling, parsely_read_field_data::ParselyReadFieldData, +}; /// A struct which represents all information needed for generating a `ParselyRead` implementation /// for a given struct. diff --git a/impl/src/code_gen/write/helpers.rs b/impl/src/code_gen/write/helpers.rs new file mode 100644 index 0000000..01b0572 --- /dev/null +++ b/impl/src/code_gen/write/helpers.rs @@ -0,0 +1,25 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use crate::model_types::MemberIdent; + +pub(crate) fn wrap_write_with_padding_handling( + element_ident: &MemberIdent, + alignment: usize, + inner: TokenStream, +) -> TokenStream { + let bytes_written_before_ident = format_ident!( + "__bytes_written_before_{}_write", + element_ident.as_friendly_string() + ); + + quote! { + let #bytes_written_before_ident = buf.remaining_mut_bytes(); + + #inner + + while (#bytes_written_before_ident - buf.remaining_mut_bytes()) % #alignment != 0 { + buf.put_u8(0).context("adding padding")?; + } + } +} diff --git a/impl/src/code_gen/write/mod.rs b/impl/src/code_gen/write/mod.rs new file mode 100644 index 0000000..4a93d47 --- /dev/null +++ b/impl/src/code_gen/write/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod helpers; +pub(crate) mod parsely_write_field_data; +pub(crate) mod parsely_write_struct_data; diff --git a/impl/src/code_gen/write/parsely_write_field_data.rs b/impl/src/code_gen/write/parsely_write_field_data.rs new file mode 100644 index 0000000..adbc0eb --- /dev/null +++ b/impl/src/code_gen/write/parsely_write_field_data.rs @@ -0,0 +1,116 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{ + code_gen::parsely_common_field_data::ParselyCommonFieldData, + model_types::{Context, ExprOrFunc, MemberIdent}, + syn_helpers::TypeExts, + ParselyWriteFieldReceiver, +}; + +use super::helpers::wrap_write_with_padding_handling; + +#[derive(Debug)] +pub(crate) struct ParselyWriteFieldData { + pub(crate) common: ParselyCommonFieldData, + /// An expression or function call that will be used to update this field in the generated + /// `StateSync` implementation for its parent type. + pub(crate) sync_expr: Option, + /// An list of expressions that should be passed as context to this field's sync method. The + /// sync method provides an opportunity to synchronize "linked" fields, where one field's value + /// depends on the value of another. + pub(crate) sync_with: Context, +} + +impl ParselyWriteFieldData { + pub(crate) fn from_receiver( + field_ident: MemberIdent, + receiver: ParselyWriteFieldReceiver, + ) -> Self { + let common = ParselyCommonFieldData { + ident: field_ident, + ty: receiver.ty, + assertion: receiver.common.assertion, + context: receiver.common.context, + map: receiver.common.map, + alignment: receiver.common.alignment, + }; + Self { + common, + sync_expr: receiver.sync_expr, + sync_with: receiver.sync_with, + } + } + + /// Get the context values that need to be passed to the read or write call for this field + pub(crate) fn sync_with_expressions(&self) -> Vec { + let field_name = self.common.ident.as_friendly_string(); + self.sync_with + .expressions(&format!("Sync context for field '{field_name}'")) + } + + /// Get this field's `sync` call expression + pub(crate) fn to_sync_call_tokens(&self) -> TokenStream { + let field_name = self.common.ident.field_name(); + let field_name_string = self.common.ident.as_friendly_string(); + if let Some(ref sync_expr) = self.sync_expr { + quote! { + self.#field_name = (#sync_expr).into_parsely_result().with_context(|| format!("Syncing field '{}'", #field_name_string))?; + } + } else if self.sync_with.is_empty() && self.common.ty.is_wrapped() { + // We'll allow this combination to skip a call to sync: for types like Option or + // Vec, synchronization is only going to make sense if a custom function was + // provided. + quote! {} + } else { + let sync_with = self.sync_with_expressions(); + quote! { + self.#field_name.sync((#(#sync_with,)*)).with_context(|| format!("Syncing field '{}'", #field_name_string))?; + } + } + } +} + +impl ToTokens for ParselyWriteFieldData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let field_name = self.common.ident.field_name(); + let field_name_string = field_name.to_string(); + let write_type = self.common.buffer_type(); + // Context values that we need to pass to this field's ParselyWrite::write method + let context_values = self.common.context_values(); + + let mut output = TokenStream::new(); + + if let Some(ref assertion) = self.common.assertion { + assertion.to_write_assertion_tokens(&field_name_string, &mut output); + } + + if let Some(ref map_expr) = self.common.map { + map_expr.to_write_map_tokens(&field_name, &mut output); + } else if self.common.ty.is_option() { + output.extend(quote! { + if let Some(ref v) = self.#field_name { + #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; + } + }); + } else if self.common.ty.is_collection() { + output.extend(quote! { + self.#field_name.iter().enumerate().map(|(idx, v)| { + #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Index {idx}")) + }).collect::>>().with_context(|| format!("Writing field '{}'", #field_name_string))?; + }); + } else { + output.extend(quote! { + #write_type::write::(&self.#field_name, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; + }); + } + + output = if let Some(alignment) = self.common.alignment { + wrap_write_with_padding_handling(&self.common.ident, alignment, output) + } else { + output + }; + + tokens.extend(output); + } +} diff --git a/impl/src/code_gen/write/parsely_write_struct_data.rs b/impl/src/code_gen/write/parsely_write_struct_data.rs new file mode 100644 index 0000000..01dbae7 --- /dev/null +++ b/impl/src/code_gen/write/parsely_write_struct_data.rs @@ -0,0 +1,112 @@ +use anyhow::anyhow; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{ + get_crate_name, + model_types::{MemberIdent, TypedFnArgList}, + ParselyWriteReceiver, +}; + +use super::{ + helpers::wrap_write_with_padding_handling, parsely_write_field_data::ParselyWriteFieldData, +}; + +pub(crate) struct ParselyWriteStructData { + pub(crate) ident: syn::Ident, + pub(crate) required_context: Option, + pub(crate) alignment: Option, + pub(crate) sync_args: Option, + pub(crate) fields: Vec, +} + +impl TryFrom for ParselyWriteStructData { + type Error = anyhow::Error; + + fn try_from(value: ParselyWriteReceiver) -> Result { + let struct_receiver_fields = value.data.take_struct().ok_or(anyhow!("Not a struct"))?; + let data_fields = struct_receiver_fields + .into_iter() + .enumerate() + .map(|(field_index, field)| { + let ident = + MemberIdent::from_ident_or_index(field.ident.as_ref(), field_index as u32); + ParselyWriteFieldData::from_receiver(ident, field) + }) + .collect::>(); + + Ok(ParselyWriteStructData { + ident: value.ident, + required_context: value.required_context, + alignment: value.alignment, + sync_args: value.sync_args, + fields: data_fields, + }) + } +} + +impl ToTokens for ParselyWriteStructData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let struct_name = &self.ident; + let (context_assignments, context_types) = + if let Some(ref required_context) = self.required_context { + (required_context.assignments(), required_context.types()) + } else { + (vec![], vec![]) + }; + + let fields = &self.fields; + let field_writes = quote! { + #(#fields)* + }; + + let sync_field_calls = fields + .iter() + .map(|f| f.to_sync_call_tokens()) + .collect::>(); + + let (sync_args_variables, sync_args_types) = if let Some(ref sync_args) = self.sync_args { + (sync_args.names(), sync_args.types()) + } else { + (vec![], vec![]) + }; + + let body = if let Some(alignment) = self.alignment { + wrap_write_with_padding_handling( + &MemberIdent::from_ident(&self.ident), + alignment, + field_writes, + ) + } else { + field_writes + }; + + tokens.extend(quote! { + impl ::#crate_name::ParselyWrite for #struct_name { + type Ctx = (#(#context_types,)*); + fn write( + &self, + buf: &mut B, + ctx: Self::Ctx, + ) -> ParselyResult<()> { + #(#context_assignments)* + + #body + + Ok(()) + } + } + + impl StateSync for #struct_name { + type SyncCtx = (#(#sync_args_types,)*); + fn sync(&mut self, (#(#sync_args_variables,)*): (#(#sync_args_types,)*)) -> ParselyResult<()> { + #(#sync_field_calls)* + + Ok(()) + } + + } + }); + } +} diff --git a/impl/src/lib.rs b/impl/src/lib.rs index cb5ccda..e8e44b7 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -26,11 +26,11 @@ pub mod anyhow { } use code_gen::{ - gen_write::generate_parsely_write_impl, read::{ parsely_read_enum_data::ParselyReadEnumData, parsely_read_struct_data::ParselyReadStructData, }, + write::parsely_write_struct_data::ParselyWriteStructData, }; use darling::{ast, FromDeriveInput, FromField, FromMeta, FromVariant}; use model_types::{Assertion, Context, ExprOrFunc, MapExpr, TypedFnArgList}; @@ -62,9 +62,18 @@ pub fn derive_parsely_read(item: TokenStream) -> std::result::Result std::result::Result { let ast: DeriveInput = syn::parse2(item)?; - let data = ParselyWriteData::from_derive_input(&ast)?; + let data = ParselyWriteReceiver::from_derive_input(&ast)?; - Ok(generate_parsely_write_impl(data)) + if data.data.is_struct() { + let struct_data = ParselyWriteStructData::try_from(data).unwrap(); + Ok(quote! { + #struct_data + }) + } else { + todo!() + } + + // Ok(generate_parsely_write_impl(data)) } #[derive(Debug, FromField, FromMeta)] @@ -121,7 +130,7 @@ pub struct ParselyReadVariantReceiver { #[derive(Debug, FromField)] #[darling(attributes(parsely, parsely_write))] -pub struct ParselyWriteFieldData { +pub struct ParselyWriteFieldReceiver { ident: Option, ty: syn::Type, @@ -140,46 +149,6 @@ pub struct ParselyWriteFieldData { sync_with: Context, } -impl ParselyWriteFieldData { - /// Get the 'buffer type' of this field (the type that will be used when reading from or - /// writing to the buffer): for wrapper types (like [`Option`] or [`Vec`]), this will be the - /// inner type. - pub(crate) fn buffer_type(&self) -> &syn::Type { - if self.ty.is_option() || self.ty.is_collection() { - self.ty - .inner_type() - .expect("Option or collection has an inner type") - } else { - &self.ty - } - } - - /// Get the context values that need to be passed to the read or write call for this field - pub(crate) fn context_values(&self) -> Vec { - let field_name = self - .ident - .as_ref() - .expect("Field must have a name") - .to_string(); - if let Some(ref field_context) = self.common.context { - field_context.expressions(&format!("Write context for field '{field_name}'")) - } else { - vec![] - } - } - - /// Get the context values that need to be passed to the read or write call for this field - pub(crate) fn sync_with_expressions(&self) -> Vec { - let field_name = self - .ident - .as_ref() - .expect("Field must have a name") - .to_string(); - self.sync_with - .expressions(&format!("Sync context for field '{field_name}'")) - } -} - #[derive(Debug, FromDeriveInput)] #[darling(attributes(parsely, parsely_read), supports(struct_any, enum_any))] pub struct ParselyReadReceiver { @@ -193,12 +162,12 @@ pub struct ParselyReadReceiver { #[derive(Debug, FromDeriveInput)] #[darling(attributes(parsely, parsely_write), supports(struct_any, enum_any))] -pub struct ParselyWriteData { +pub struct ParselyWriteReceiver { ident: syn::Ident, required_context: Option, sync_args: Option, alignment: Option, - data: ast::Data<(), ParselyWriteFieldData>, + data: ast::Data<(), ParselyWriteFieldReceiver>, } pub(crate) fn get_crate_name() -> syn::Ident { diff --git a/impl/src/model_types.rs b/impl/src/model_types.rs index 82dc930..3a2ef74 100644 --- a/impl/src/model_types.rs +++ b/impl/src/model_types.rs @@ -334,49 +334,6 @@ impl Assertion { } } -pub(crate) fn wrap_read_with_padding_handling( - element_ident: &MemberIdent, - alignment: usize, - inner: TokenStream, -) -> TokenStream { - let bytes_read_before_ident = format_ident!( - "__bytes_read_before_{}_read", - element_ident.as_friendly_string() - ); - quote! { - let #bytes_read_before_ident = buf.remaining_bytes(); - - #inner - - while (#bytes_read_before_ident - buf.remaining_bytes()) % #alignment != 0 { - buf.get_u8().context("consuming padding")?; - } - } -} - -pub(crate) fn wrap_write_with_padding_handling( - element_ident: &syn::Ident, - alignment: usize, - inner: TokenStream, -) -> TokenStream { - let bytes_written_before_ident = format_ident!("__bytes_written_before_{element_ident}_write"); - let bytes_written_after_ident = format_ident!("__bytes_written_after_{element_ident}_write"); - let amount_written_ident = format_ident!("__bytes_written_for_{element_ident}"); - - quote! { - let #bytes_written_before_ident = buf.remaining_bytes(); - - #inner - - let #bytes_written_after_ident = buf.remaining_bytes(); - let mut #amount_written_ident = #bytes_written_after_ident - #bytes_written_before_ident; - while #amount_written_ident % #alignment != 0 { - buf.put_u8(0).context("padding")?; - #amount_written_ident += 1; - } - } -} - #[derive(Debug)] pub(crate) enum MemberIdent { Named(syn::Ident), @@ -419,8 +376,6 @@ impl MemberIdent { /// Return the value of this `MemberIdent` in the form of a `syn::Ident` such that it can be /// used to access this field inside the containing structure or enum. E.g. for a named /// variable it will be the field's name, for an unnamed variable it will be the field's index. - /// TODO: i think we'll use this for the write impls, so allow it to be dead for now - #[allow(dead_code)] pub fn field_name(&self) -> syn::Ident { match self { MemberIdent::Named(ident) => ident.clone(), diff --git a/tests/expand/alignment.expanded.rs b/tests/expand/alignment.expanded.rs index 3f42ad1..0fa9a56 100644 --- a/tests/expand/alignment.expanded.rs +++ b/tests/expand/alignment.expanded.rs @@ -20,7 +20,7 @@ impl ::parsely_rs::ParselyRead for Foo { impl ::parsely_rs::ParselyWrite for Foo { type Ctx = (); fn write(&self, buf: &mut B, ctx: Self::Ctx) -> ParselyResult<()> { - let __bytes_remaining_start = buf.remaining_mut_bytes(); + let __bytes_written_before_Foo_write = buf.remaining_mut_bytes(); u8::write::(&self.one, buf, ()) .with_context(|| ::alloc::__export::must_use({ let res = ::alloc::fmt::format( @@ -28,8 +28,10 @@ impl ::parsely_rs::ParselyWrite for Foo { ); res }))?; - while (__bytes_remaining_start - buf.remaining_mut_bytes()) % 4usize != 0 { - let _ = buf.put_u8(0).context("padding")?; + while (__bytes_written_before_Foo_write - buf.remaining_mut_bytes()) % 4usize + != 0 + { + buf.put_u8(0).context("adding padding")?; } Ok(()) } From 214a88743aece48b2712031542e04e04ddeb358a Mon Sep 17 00:00:00 2001 From: bbaldino Date: Thu, 29 May 2025 14:04:30 -0700 Subject: [PATCH 07/10] refactor: destructure context argument into variables instead of separate assignments in method body --- .../code_gen/read/parsely_read_enum_data.rs | 15 ++++----------- .../code_gen/read/parsely_read_struct_data.rs | 19 +++++-------------- .../write/parsely_write_struct_data.rs | 7 +++---- impl/src/lib.rs | 2 -- impl/src/model_types.rs | 15 --------------- tests/expand/alignment.expanded.rs | 4 ++-- tests/expand/assertion.expanded.rs | 4 ++-- tests/expand/map.expanded.rs | 4 ++-- tests/expand/read_context.expanded.rs | 17 +++++++++++++++++ tests/expand/read_context.rs | 8 ++++++++ tests/ui/pass/read_context.rs | 15 +++++++++++++++ 11 files changed, 58 insertions(+), 52 deletions(-) create mode 100644 tests/expand/read_context.expanded.rs create mode 100644 tests/expand/read_context.rs create mode 100644 tests/ui/pass/read_context.rs diff --git a/impl/src/code_gen/read/parsely_read_enum_data.rs b/impl/src/code_gen/read/parsely_read_enum_data.rs index e1121e9..33c485a 100644 --- a/impl/src/code_gen/read/parsely_read_enum_data.rs +++ b/impl/src/code_gen/read/parsely_read_enum_data.rs @@ -1,6 +1,6 @@ use ::anyhow::anyhow; use proc_macro2::TokenStream; -use quote::{format_ident, quote, ToTokens}; +use quote::{quote, ToTokens}; use crate::{ anyhow, get_crate_name, model_types::MemberIdent, ParselyReadReceiver, TypedFnArgList, @@ -71,19 +71,14 @@ impl ToTokens for ParselyReadEnumData { fn to_tokens(&self, tokens: &mut TokenStream) { let crate_name = get_crate_name(); let enum_name = &self.ident; - let (context_assignments, context_types) = + let (context_variables, context_types) = if let Some(ref required_context) = self.required_context { - (required_context.assignments(), required_context.types()) + (required_context.names(), required_context.types()) } else { (Vec::new(), Vec::new()) }; let match_value = &self.key; - let ctx_var = if context_types.is_empty() { - format_ident!("_ctx") - } else { - format_ident!("ctx") - }; let match_arms = &self.variants; let body = quote! { @@ -105,9 +100,7 @@ impl ToTokens for ParselyReadEnumData { tokens.extend(quote! { impl ::#crate_name::ParselyRead for #enum_name { type Ctx = (#(#context_types,)*); - fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { - #(#context_assignments)* - + fn read(buf: &mut B, (#(#context_variables,)*): (#(#context_types,)*)) -> ::#crate_name::ParselyResult { #body } } diff --git a/impl/src/code_gen/read/parsely_read_struct_data.rs b/impl/src/code_gen/read/parsely_read_struct_data.rs index 40e33d4..98eb6a0 100644 --- a/impl/src/code_gen/read/parsely_read_struct_data.rs +++ b/impl/src/code_gen/read/parsely_read_struct_data.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use proc_macro2::TokenStream; -use quote::{format_ident, quote, ToTokens}; +use quote::{quote, ToTokens}; use crate::{get_crate_name, model_types::MemberIdent, ParselyReadReceiver, TypedFnArgList}; @@ -52,9 +52,9 @@ impl ToTokens for ParselyReadStructData { let struct_name = &self.ident; // Extract out the assignment expressions we'll do to assign the values of the context tuple // to the configured variable names, as well as the types of the context tuple. - let (context_assignments, context_types) = + let (context_variables, context_types) = if let Some(ref required_context) = self.required_context { - (required_context.assignments(), required_context.types()) + (required_context.names(), required_context.types()) } else { (Vec::new(), Vec::new()) }; @@ -73,11 +73,6 @@ impl ToTokens for ParselyReadStructData { } else { field_reads }; - let ctx_var = if context_types.is_empty() { - format_ident!("_ctx") - } else { - format_ident!("ctx") - }; let field_names = fields .iter() @@ -89,9 +84,7 @@ impl ToTokens for ParselyReadStructData { tokens.extend(quote! { impl ::#crate_name::ParselyRead for #struct_name { type Ctx = (#(#context_types,)*); - fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { - #(#context_assignments)* - + fn read(buf: &mut B, (#(#context_variables,)*): (#(#context_types,)*)) -> ::#crate_name::ParselyResult { #body Ok(Self { #(#field_names,)* }) @@ -102,9 +95,7 @@ impl ToTokens for ParselyReadStructData { tokens.extend(quote! { impl ::#crate_name::ParselyRead for #struct_name { type Ctx = (#(#context_types,)*); - fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { - #(#context_assignments)* - + fn read(buf: &mut B, (#(#context_variables,)*): (#(#context_types,)*)) -> ::#crate_name::ParselyResult { #body Ok(Self(#(#field_names,)* )) diff --git a/impl/src/code_gen/write/parsely_write_struct_data.rs b/impl/src/code_gen/write/parsely_write_struct_data.rs index 01dbae7..903ad2b 100644 --- a/impl/src/code_gen/write/parsely_write_struct_data.rs +++ b/impl/src/code_gen/write/parsely_write_struct_data.rs @@ -49,9 +49,9 @@ impl ToTokens for ParselyWriteStructData { fn to_tokens(&self, tokens: &mut TokenStream) { let crate_name = get_crate_name(); let struct_name = &self.ident; - let (context_assignments, context_types) = + let (context_variables, context_types) = if let Some(ref required_context) = self.required_context { - (required_context.assignments(), required_context.types()) + (required_context.names(), required_context.types()) } else { (vec![], vec![]) }; @@ -88,9 +88,8 @@ impl ToTokens for ParselyWriteStructData { fn write( &self, buf: &mut B, - ctx: Self::Ctx, + (#(#context_variables,)*): Self::Ctx, ) -> ParselyResult<()> { - #(#context_assignments)* #body diff --git a/impl/src/lib.rs b/impl/src/lib.rs index e8e44b7..f3452fc 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -72,8 +72,6 @@ pub fn derive_parsely_write(item: TokenStream) -> std::result::Result Vec<&syn::Ident> { self.0.iter().map(|t| t.name()).collect() } - - // TODO: this is context-specific, but now this type is more generic. move it? - pub(crate) fn assignments(&self) -> Vec { - self.0 - .iter() - .enumerate() - .map(|(idx, fn_arg)| { - let idx: syn::Index = idx.into(); - syn::parse2::(quote! { - let #fn_arg = ctx.#idx; - }) - .unwrap() - }) - .collect::>() - } } impl FromMeta for TypedFnArgList { diff --git a/tests/expand/alignment.expanded.rs b/tests/expand/alignment.expanded.rs index 0fa9a56..efb6bd6 100644 --- a/tests/expand/alignment.expanded.rs +++ b/tests/expand/alignment.expanded.rs @@ -7,7 +7,7 @@ impl ::parsely_rs::ParselyRead for Foo { type Ctx = (); fn read( buf: &mut B, - _ctx: (), + (): (), ) -> ::parsely_rs::ParselyResult { let __bytes_read_before_Foo_read = buf.remaining_bytes(); let one = u8::read::(buf, ()).with_context(|| "Reading field 'one'")?; @@ -19,7 +19,7 @@ impl ::parsely_rs::ParselyRead for Foo { } impl ::parsely_rs::ParselyWrite for Foo { type Ctx = (); - fn write(&self, buf: &mut B, ctx: Self::Ctx) -> ParselyResult<()> { + fn write(&self, buf: &mut B, (): Self::Ctx) -> ParselyResult<()> { let __bytes_written_before_Foo_write = buf.remaining_mut_bytes(); u8::write::(&self.one, buf, ()) .with_context(|| ::alloc::__export::must_use({ diff --git a/tests/expand/assertion.expanded.rs b/tests/expand/assertion.expanded.rs index de4eb2c..a72c02e 100644 --- a/tests/expand/assertion.expanded.rs +++ b/tests/expand/assertion.expanded.rs @@ -7,7 +7,7 @@ impl ::parsely_rs::ParselyRead for Foo { type Ctx = (); fn read( buf: &mut B, - _ctx: (), + (): (), ) -> ::parsely_rs::ParselyResult { let value = u8::read::(buf, ()) .and_then(|read_value| { @@ -35,7 +35,7 @@ impl ::parsely_rs::ParselyRead for Foo { } impl ::parsely_rs::ParselyWrite for Foo { type Ctx = (); - fn write(&self, buf: &mut B, ctx: Self::Ctx) -> ParselyResult<()> { + fn write(&self, buf: &mut B, (): Self::Ctx) -> ParselyResult<()> { let __value_assertion_func = |v: &u8| *v % 2 == 0; if !__value_assertion_func(&self.value) { return ::anyhow::__private::Err( diff --git a/tests/expand/map.expanded.rs b/tests/expand/map.expanded.rs index d672265..f892686 100644 --- a/tests/expand/map.expanded.rs +++ b/tests/expand/map.expanded.rs @@ -8,7 +8,7 @@ impl ::parsely_rs::ParselyRead for Foo { type Ctx = (); fn read( buf: &mut B, - _ctx: (), + (): (), ) -> ::parsely_rs::ParselyResult { let value = { let original_value = ::parsely_rs::ParselyRead::read::(buf, ()) @@ -33,7 +33,7 @@ impl ::parsely_rs::ParselyRead for Foo { } impl ::parsely_rs::ParselyWrite for Foo { type Ctx = (); - fn write(&self, buf: &mut B, ctx: Self::Ctx) -> ParselyResult<()> { + fn write(&self, buf: &mut B, (): Self::Ctx) -> ParselyResult<()> { { let mapped_value = (|v: &str| { v.parse::() })(&self.value); let result = <_ as IntoWritableParselyResult< diff --git a/tests/expand/read_context.expanded.rs b/tests/expand/read_context.expanded.rs new file mode 100644 index 0000000..da968ca --- /dev/null +++ b/tests/expand/read_context.expanded.rs @@ -0,0 +1,17 @@ +use parsely_rs::*; +#[parsely_read(required_context("some_context_value: u8"))] +struct ReadContext { + #[parsely_read(assign_from = "some_context_value")] + one: u8, +} +impl ::parsely_rs::ParselyRead for ReadContext { + type Ctx = (u8,); + fn read( + buf: &mut B, + (some_context_value,): (u8,), + ) -> ::parsely_rs::ParselyResult { + let one = ParselyResult::<_>::Ok(some_context_value) + .with_context(|| "Reading field 'one'")?; + Ok(Self { one }) + } +} diff --git a/tests/expand/read_context.rs b/tests/expand/read_context.rs new file mode 100644 index 0000000..1216793 --- /dev/null +++ b/tests/expand/read_context.rs @@ -0,0 +1,8 @@ +use parsely_rs::*; + +#[derive(ParselyRead)] +#[parsely_read(required_context("some_context_value: u8"))] +struct ReadContext { + #[parsely_read(assign_from = "some_context_value")] + one: u8, +} diff --git a/tests/ui/pass/read_context.rs b/tests/ui/pass/read_context.rs new file mode 100644 index 0000000..225a7fe --- /dev/null +++ b/tests/ui/pass/read_context.rs @@ -0,0 +1,15 @@ +use parsely_rs::*; + +#[derive(ParselyRead)] +#[parsely_read(required_context("some_context_value: u8"))] +struct ReadContext { + #[parsely_read(assign_from = "some_context_value")] + one: u8, +} + +fn main() { + let mut buf = Bits::from_static_bytes(&[]); + + let value = ReadContext::read::(&mut buf, (42,)).expect("successful parse"); + assert_eq!(value.one, 42); +} From 2f9a44d8c3c5c46b7b835964194a7dec7dd2c67d Mon Sep 17 00:00:00 2001 From: bbaldino Date: Mon, 2 Jun 2025 15:36:20 -0700 Subject: [PATCH 08/10] refactor: redo write code gen, support newtype/tuple structs and enums --- .../src/code_gen/parsely_common_field_data.rs | 9 +- impl/src/code_gen/read/helpers.rs | 4 +- .../code_gen/read/parsely_read_enum_data.rs | 20 ++-- .../code_gen/read/parsely_read_field_data.rs | 5 +- .../code_gen/read/parsely_read_struct_data.rs | 15 +-- .../read/parsely_read_variant_data.rs | 6 +- impl/src/code_gen/write/helpers.rs | 10 +- impl/src/code_gen/write/mod.rs | 2 + .../code_gen/write/parsely_write_enum_data.rs | 111 ++++++++++++++++++ .../write/parsely_write_field_data.rs | 45 ++++--- .../write/parsely_write_struct_data.rs | 32 ++--- .../write/parsely_write_variant_data.rs | 63 ++++++++++ impl/src/lib.rs | 31 +++-- impl/src/model_types.rs | 77 +++--------- impl/src/syn_helpers.rs | 39 ++++++ tests/expand/alignment.expanded.rs | 2 +- tests/expand/assertion.expanded.rs | 2 +- tests/expand/map.expanded.rs | 2 +- tests/ui/pass/enum_basic.rs | 7 +- tests/ui/pass/newtype_struct.rs | 8 +- tests/ui/pass/tuple_struct.rs | 9 +- 21 files changed, 357 insertions(+), 142 deletions(-) create mode 100644 impl/src/code_gen/write/parsely_write_enum_data.rs create mode 100644 impl/src/code_gen/write/parsely_write_variant_data.rs diff --git a/impl/src/code_gen/parsely_common_field_data.rs b/impl/src/code_gen/parsely_common_field_data.rs index 9be7210..4586918 100644 --- a/impl/src/code_gen/parsely_common_field_data.rs +++ b/impl/src/code_gen/parsely_common_field_data.rs @@ -1,9 +1,12 @@ -use crate::{model_types::MemberIdent, syn_helpers::TypeExts, Assertion, Context, MapExpr}; +use crate::{ + syn_helpers::{MemberExts, TypeExts}, + Assertion, Context, MapExpr, +}; /// Items that are needed for both reading and writing a field to/from a buffer. #[derive(Debug)] pub(crate) struct ParselyCommonFieldData { - pub(crate) ident: MemberIdent, + pub(crate) ident: syn::Member, /// The field's type pub(crate) ty: syn::Type, @@ -37,7 +40,7 @@ impl ParselyCommonFieldData { if let Some(ref field_context) = self.context { field_context.expressions(&format!( "Read context for field '{}'", - self.ident.as_friendly_string() + self.ident.as_friendly_string(), )) } else { vec![] diff --git a/impl/src/code_gen/read/helpers.rs b/impl/src/code_gen/read/helpers.rs index ce25815..a4db94e 100644 --- a/impl/src/code_gen/read/helpers.rs +++ b/impl/src/code_gen/read/helpers.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use crate::model_types::{CollectionLimit, MemberIdent}; +use crate::{model_types::CollectionLimit, syn_helpers::MemberExts}; pub(crate) fn generate_plain_read(ty: &syn::Type, context_values: &[syn::Expr]) -> TokenStream { quote! { @@ -48,7 +48,7 @@ pub(crate) fn generate_collection_read( } pub(crate) fn wrap_read_with_padding_handling( - element_ident: &MemberIdent, + element_ident: &syn::Member, alignment: usize, inner: TokenStream, ) -> TokenStream { diff --git a/impl/src/code_gen/read/parsely_read_enum_data.rs b/impl/src/code_gen/read/parsely_read_enum_data.rs index 33c485a..1e8f79b 100644 --- a/impl/src/code_gen/read/parsely_read_enum_data.rs +++ b/impl/src/code_gen/read/parsely_read_enum_data.rs @@ -2,9 +2,7 @@ use ::anyhow::anyhow; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use crate::{ - anyhow, get_crate_name, model_types::MemberIdent, ParselyReadReceiver, TypedFnArgList, -}; +use crate::{anyhow, get_crate_name, syn_helpers::MemberExts, ParselyReadReceiver, TypedFnArgList}; use super::{ helpers::wrap_read_with_padding_handling, parsely_read_field_data::ParselyReadFieldData, @@ -16,7 +14,7 @@ use super::{ #[derive(Debug)] pub(crate) struct ParselyReadEnumData { pub(crate) ident: syn::Ident, - pub(crate) required_context: Option, + pub(crate) required_context: TypedFnArgList, pub(crate) alignment: Option, pub(crate) key: syn::Expr, pub(crate) variants: Vec, @@ -40,7 +38,7 @@ impl TryFrom for ParselyReadEnumData { .into_iter() .enumerate() .map(|(field_index, field)| { - let ident = MemberIdent::from_ident_or_index( + let ident = syn::Member::from_ident_or_index( field.ident.as_ref(), field_index as u32, ); @@ -72,11 +70,7 @@ impl ToTokens for ParselyReadEnumData { let crate_name = get_crate_name(); let enum_name = &self.ident; let (context_variables, context_types) = - if let Some(ref required_context) = self.required_context { - (required_context.names(), required_context.types()) - } else { - (Vec::new(), Vec::new()) - }; + (self.required_context.names(), self.required_context.types()); let match_value = &self.key; @@ -89,7 +83,11 @@ impl ToTokens for ParselyReadEnumData { }; let body = if let Some(alignment) = self.alignment { - wrap_read_with_padding_handling(&MemberIdent::from_ident(&self.ident), alignment, body) + wrap_read_with_padding_handling( + &syn::Member::Named(self.ident.clone()), + alignment, + body, + ) } else { body }; diff --git a/impl/src/code_gen/read/parsely_read_field_data.rs b/impl/src/code_gen/read/parsely_read_field_data.rs index d152e56..3ed95d1 100644 --- a/impl/src/code_gen/read/parsely_read_field_data.rs +++ b/impl/src/code_gen/read/parsely_read_field_data.rs @@ -3,7 +3,8 @@ use quote::{quote, ToTokens}; use crate::{ code_gen::{helpers::wrap_in_optional, parsely_common_field_data::ParselyCommonFieldData}, - model_types::{CollectionLimit, MemberIdent}, + model_types::CollectionLimit, + syn_helpers::MemberExts, ParselyReadFieldReceiver, TypeExts, }; @@ -28,7 +29,7 @@ pub struct ParselyReadFieldData { impl ParselyReadFieldData { pub(crate) fn from_receiver( - field_ident: MemberIdent, + field_ident: syn::Member, receiver: ParselyReadFieldReceiver, ) -> Self { let collection_limit = if receiver.ty.is_collection() { diff --git a/impl/src/code_gen/read/parsely_read_struct_data.rs b/impl/src/code_gen/read/parsely_read_struct_data.rs index 98eb6a0..1ffb55b 100644 --- a/impl/src/code_gen/read/parsely_read_struct_data.rs +++ b/impl/src/code_gen/read/parsely_read_struct_data.rs @@ -2,7 +2,8 @@ use anyhow::anyhow; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use crate::{get_crate_name, model_types::MemberIdent, ParselyReadReceiver, TypedFnArgList}; +use crate::syn_helpers::MemberExts; +use crate::{get_crate_name, ParselyReadReceiver, TypedFnArgList}; use super::{ helpers::wrap_read_with_padding_handling, parsely_read_field_data::ParselyReadFieldData, @@ -13,7 +14,7 @@ use super::{ pub(crate) struct ParselyReadStructData { pub(crate) ident: syn::Ident, pub(crate) style: darling::ast::Style, - pub(crate) required_context: Option, + pub(crate) required_context: TypedFnArgList, pub(crate) alignment: Option, pub(crate) fields: Vec, } @@ -32,7 +33,7 @@ impl TryFrom for ParselyReadStructData { .enumerate() .map(|(field_index, field)| { let ident = - MemberIdent::from_ident_or_index(field.ident.as_ref(), field_index as u32); + syn::Member::from_ident_or_index(field.ident.as_ref(), field_index as u32); ParselyReadFieldData::from_receiver(ident, field) }) .collect::>(); @@ -53,11 +54,7 @@ impl ToTokens for ParselyReadStructData { // Extract out the assignment expressions we'll do to assign the values of the context tuple // to the configured variable names, as well as the types of the context tuple. let (context_variables, context_types) = - if let Some(ref required_context) = self.required_context { - (required_context.names(), required_context.types()) - } else { - (Vec::new(), Vec::new()) - }; + (self.required_context.names(), self.required_context.types()); let fields = &self.fields; let field_reads = quote! { @@ -66,7 +63,7 @@ impl ToTokens for ParselyReadStructData { let body = if let Some(alignment) = self.alignment { wrap_read_with_padding_handling( - &MemberIdent::from_ident(&self.ident), + &syn::Member::Named(self.ident.clone()), alignment, field_reads, ) diff --git a/impl/src/code_gen/read/parsely_read_variant_data.rs b/impl/src/code_gen/read/parsely_read_variant_data.rs index ba80cb4..0cd4609 100644 --- a/impl/src/code_gen/read/parsely_read_variant_data.rs +++ b/impl/src/code_gen/read/parsely_read_variant_data.rs @@ -1,8 +1,8 @@ -use crate::model_types::MemberIdent; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use super::parsely_read_field_data::ParselyReadFieldData; +use crate::syn_helpers::MemberExts; #[derive(Debug)] pub(crate) struct ParselyReadVariantData { @@ -14,10 +14,11 @@ pub(crate) struct ParselyReadVariantData { } impl ParselyReadVariantData { + /// Returns true if this variant contains named fields, false otherwise fn named_fields(&self) -> bool { self.fields .iter() - .any(|f| matches!(f.common.ident, MemberIdent::Named(_))) + .any(|f| matches!(f.common.ident, syn::Member::Named(_))) } } @@ -43,6 +44,7 @@ impl ToTokens for ParselyReadVariantData { .collect::>(); let enum_name = &self.enum_name; let variant_name = &self.ident; + // TODO: don't think we're handling discriminant correctly here if self.fields.is_empty() { tokens.extend(quote! { #arm_expr => { diff --git a/impl/src/code_gen/write/helpers.rs b/impl/src/code_gen/write/helpers.rs index 01b0572..5d894d3 100644 --- a/impl/src/code_gen/write/helpers.rs +++ b/impl/src/code_gen/write/helpers.rs @@ -1,10 +1,10 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use crate::model_types::MemberIdent; +use crate::syn_helpers::MemberExts; pub(crate) fn wrap_write_with_padding_handling( - element_ident: &MemberIdent, + element_ident: &syn::Member, alignment: usize, inner: TokenStream, ) -> TokenStream { @@ -23,3 +23,9 @@ pub(crate) fn wrap_write_with_padding_handling( } } } + +#[derive(Debug)] +pub(crate) enum ParentType { + Struct, + Enum, +} diff --git a/impl/src/code_gen/write/mod.rs b/impl/src/code_gen/write/mod.rs index 4a93d47..47c1101 100644 --- a/impl/src/code_gen/write/mod.rs +++ b/impl/src/code_gen/write/mod.rs @@ -1,3 +1,5 @@ pub(crate) mod helpers; +pub(crate) mod parsely_write_enum_data; pub(crate) mod parsely_write_field_data; pub(crate) mod parsely_write_struct_data; +pub(crate) mod parsely_write_variant_data; diff --git a/impl/src/code_gen/write/parsely_write_enum_data.rs b/impl/src/code_gen/write/parsely_write_enum_data.rs new file mode 100644 index 0000000..e552e67 --- /dev/null +++ b/impl/src/code_gen/write/parsely_write_enum_data.rs @@ -0,0 +1,111 @@ +use anyhow::anyhow; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{ + get_crate_name, model_types::TypedFnArgList, syn_helpers::MemberExts, ParselyWriteReceiver, +}; + +use super::{ + helpers::{wrap_write_with_padding_handling, ParentType}, + parsely_write_field_data::ParselyWriteFieldData, + parsely_write_variant_data::ParselyWriteVariantData, +}; + +pub(crate) struct ParselyWriteEnumData { + pub(crate) ident: syn::Ident, + pub(crate) required_context: TypedFnArgList, + pub(crate) alignment: Option, + pub(crate) sync_args: TypedFnArgList, + pub(crate) variants: Vec, +} + +impl TryFrom for ParselyWriteEnumData { + type Error = anyhow::Error; + + fn try_from(value: ParselyWriteReceiver) -> Result { + let variants = value + .data + .take_enum() + .ok_or(anyhow!("Not an enum"))? + .into_iter() + .map(|v| { + let data_fields = v + .fields + .into_iter() + .enumerate() + .map(|(field_index, field)| { + let ident = syn::Member::from_ident_or_index( + field.ident.as_ref(), + field_index as u32, + ); + ParselyWriteFieldData::from_receiver(ident, ParentType::Enum, field) + }) + .collect::>(); + ParselyWriteVariantData { + enum_name: value.ident.clone(), + ident: v.ident, + discriminant: v.discriminant, + fields: data_fields, + } + }) + .collect::>(); + Ok(ParselyWriteEnumData { + ident: value.ident, + required_context: value.required_context, + alignment: value.alignment, + sync_args: value.sync_args, + variants, + }) + } +} + +impl ToTokens for ParselyWriteEnumData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let enum_name = &self.ident; + let (context_variables, context_types) = + (self.required_context.names(), self.required_context.types()); + let match_arms = &self.variants; + + let body = quote! { + match self { + #(#match_arms)* + _ => ParselyResult::<()>::Err(anyhow!("No arms matched self"))?, + } + }; + + let body = if let Some(alignment) = self.alignment { + wrap_write_with_padding_handling( + &syn::Member::Named(self.ident.clone()), + alignment, + body, + ) + } else { + body + }; + + let (sync_args_variables, sync_args_types) = + (self.sync_args.names(), self.sync_args.types()); + + // TODO: need to think about what the sync impl for an enum should look like and finish + // that + tokens.extend(quote! { + impl ::#crate_name::ParselyWrite for #enum_name { + type Ctx = (#(#context_types,)*); + fn write(&self, buf: &mut B, (#(#context_variables,)*): Self::Ctx,) -> ParselyResult<()> { + #body + + Ok(()) + } + } + + impl ::#crate_name::StateSync for #enum_name { + type SyncCtx = (#(#sync_args_types,)*); + fn sync(&mut self, (#(#sync_args_variables,)*): (#(#sync_args_types,)*)) -> ParselyResult<()> { + Ok(()) + } + } + }); + } +} diff --git a/impl/src/code_gen/write/parsely_write_field_data.rs b/impl/src/code_gen/write/parsely_write_field_data.rs index adbc0eb..c7befa4 100644 --- a/impl/src/code_gen/write/parsely_write_field_data.rs +++ b/impl/src/code_gen/write/parsely_write_field_data.rs @@ -3,16 +3,19 @@ use quote::{quote, ToTokens}; use crate::{ code_gen::parsely_common_field_data::ParselyCommonFieldData, - model_types::{Context, ExprOrFunc, MemberIdent}, - syn_helpers::TypeExts, + model_types::{Context, ExprOrFunc}, + syn_helpers::{MemberExts, TypeExts}, ParselyWriteFieldReceiver, }; -use super::helpers::wrap_write_with_padding_handling; +use super::helpers::{wrap_write_with_padding_handling, ParentType}; #[derive(Debug)] pub(crate) struct ParselyWriteFieldData { pub(crate) common: ParselyCommonFieldData, + /// The way we refer to a field when writing differs between structs and enums, so track what + /// kind this field belongs to + pub(crate) parent_type: ParentType, /// An expression or function call that will be used to update this field in the generated /// `StateSync` implementation for its parent type. pub(crate) sync_expr: Option, @@ -24,7 +27,8 @@ pub(crate) struct ParselyWriteFieldData { impl ParselyWriteFieldData { pub(crate) fn from_receiver( - field_ident: MemberIdent, + field_ident: syn::Member, + parent_type: ParentType, receiver: ParselyWriteFieldReceiver, ) -> Self { let common = ParselyCommonFieldData { @@ -37,12 +41,13 @@ impl ParselyWriteFieldData { }; Self { common, + parent_type, sync_expr: receiver.sync_expr, sync_with: receiver.sync_with, } } - /// Get the context values that need to be passed to the read or write call for this field + /// Get the context values that need to be passed to the sync call for this field pub(crate) fn sync_with_expressions(&self) -> Vec { let field_name = self.common.ident.as_friendly_string(); self.sync_with @@ -51,11 +56,11 @@ impl ParselyWriteFieldData { /// Get this field's `sync` call expression pub(crate) fn to_sync_call_tokens(&self) -> TokenStream { - let field_name = self.common.ident.field_name(); - let field_name_string = self.common.ident.as_friendly_string(); + let field_ident = &self.common.ident; + let field_name_string = field_ident.as_friendly_string(); if let Some(ref sync_expr) = self.sync_expr { quote! { - self.#field_name = (#sync_expr).into_parsely_result().with_context(|| format!("Syncing field '{}'", #field_name_string))?; + self.#field_ident = (#sync_expr).into_parsely_result().with_context(|| format!("Syncing field '{}'", #field_name_string))?; } } else if self.sync_with.is_empty() && self.common.ty.is_wrapped() { // We'll allow this combination to skip a call to sync: for types like Option or @@ -65,7 +70,7 @@ impl ParselyWriteFieldData { } else { let sync_with = self.sync_with_expressions(); quote! { - self.#field_name.sync((#(#sync_with,)*)).with_context(|| format!("Syncing field '{}'", #field_name_string))?; + self.#field_ident.sync((#(#sync_with,)*)).with_context(|| format!("Syncing field '{}'", #field_name_string))?; } } } @@ -73,40 +78,46 @@ impl ParselyWriteFieldData { impl ToTokens for ParselyWriteFieldData { fn to_tokens(&self, tokens: &mut TokenStream) { - let field_name = self.common.ident.field_name(); - let field_name_string = field_name.to_string(); + let field_ident = &self.common.ident; + let field_name_string = field_ident.as_friendly_string(); let write_type = self.common.buffer_type(); // Context values that we need to pass to this field's ParselyWrite::write method let context_values = self.common.context_values(); let mut output = TokenStream::new(); + let field_var = if matches!(self.parent_type, ParentType::Struct) { + quote! { self.#field_ident } + } else { + let field_name = field_ident.as_variable_name(); + quote! { #field_name } + }; if let Some(ref assertion) = self.common.assertion { - assertion.to_write_assertion_tokens(&field_name_string, &mut output); + assertion.to_write_assertion_tokens(&self.common.ident, &mut output); } if let Some(ref map_expr) = self.common.map { - map_expr.to_write_map_tokens(&field_name, &mut output); + map_expr.to_write_map_tokens(&self.common.ident, &mut output); } else if self.common.ty.is_option() { output.extend(quote! { - if let Some(ref v) = self.#field_name { + if let Some(ref v) = #field_var { #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; } }); } else if self.common.ty.is_collection() { output.extend(quote! { - self.#field_name.iter().enumerate().map(|(idx, v)| { + #field_var.iter().enumerate().map(|(idx, v)| { #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Index {idx}")) }).collect::>>().with_context(|| format!("Writing field '{}'", #field_name_string))?; }); } else { output.extend(quote! { - #write_type::write::(&self.#field_name, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; + #write_type::write::(&#field_var, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; }); } output = if let Some(alignment) = self.common.alignment { - wrap_write_with_padding_handling(&self.common.ident, alignment, output) + wrap_write_with_padding_handling(field_ident, alignment, output) } else { output }; diff --git a/impl/src/code_gen/write/parsely_write_struct_data.rs b/impl/src/code_gen/write/parsely_write_struct_data.rs index 903ad2b..b7dde74 100644 --- a/impl/src/code_gen/write/parsely_write_struct_data.rs +++ b/impl/src/code_gen/write/parsely_write_struct_data.rs @@ -3,20 +3,19 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use crate::{ - get_crate_name, - model_types::{MemberIdent, TypedFnArgList}, - ParselyWriteReceiver, + get_crate_name, model_types::TypedFnArgList, syn_helpers::MemberExts, ParselyWriteReceiver, }; use super::{ - helpers::wrap_write_with_padding_handling, parsely_write_field_data::ParselyWriteFieldData, + helpers::{wrap_write_with_padding_handling, ParentType}, + parsely_write_field_data::ParselyWriteFieldData, }; pub(crate) struct ParselyWriteStructData { pub(crate) ident: syn::Ident, - pub(crate) required_context: Option, + pub(crate) required_context: TypedFnArgList, pub(crate) alignment: Option, - pub(crate) sync_args: Option, + pub(crate) sync_args: TypedFnArgList, pub(crate) fields: Vec, } @@ -30,8 +29,8 @@ impl TryFrom for ParselyWriteStructData { .enumerate() .map(|(field_index, field)| { let ident = - MemberIdent::from_ident_or_index(field.ident.as_ref(), field_index as u32); - ParselyWriteFieldData::from_receiver(ident, field) + syn::Member::from_ident_or_index(field.ident.as_ref(), field_index as u32); + ParselyWriteFieldData::from_receiver(ident, ParentType::Struct, field) }) .collect::>(); @@ -50,11 +49,7 @@ impl ToTokens for ParselyWriteStructData { let crate_name = get_crate_name(); let struct_name = &self.ident; let (context_variables, context_types) = - if let Some(ref required_context) = self.required_context { - (required_context.names(), required_context.types()) - } else { - (vec![], vec![]) - }; + (self.required_context.names(), self.required_context.types()); let fields = &self.fields; let field_writes = quote! { @@ -66,15 +61,12 @@ impl ToTokens for ParselyWriteStructData { .map(|f| f.to_sync_call_tokens()) .collect::>(); - let (sync_args_variables, sync_args_types) = if let Some(ref sync_args) = self.sync_args { - (sync_args.names(), sync_args.types()) - } else { - (vec![], vec![]) - }; + let (sync_args_variables, sync_args_types) = + (self.sync_args.names(), self.sync_args.types()); let body = if let Some(alignment) = self.alignment { wrap_write_with_padding_handling( - &MemberIdent::from_ident(&self.ident), + &syn::Member::Named(self.ident.clone()), alignment, field_writes, ) @@ -97,7 +89,7 @@ impl ToTokens for ParselyWriteStructData { } } - impl StateSync for #struct_name { + impl ::#crate_name::StateSync for #struct_name { type SyncCtx = (#(#sync_args_types,)*); fn sync(&mut self, (#(#sync_args_variables,)*): (#(#sync_args_types,)*)) -> ParselyResult<()> { #(#sync_field_calls)* diff --git a/impl/src/code_gen/write/parsely_write_variant_data.rs b/impl/src/code_gen/write/parsely_write_variant_data.rs new file mode 100644 index 0000000..aed89e4 --- /dev/null +++ b/impl/src/code_gen/write/parsely_write_variant_data.rs @@ -0,0 +1,63 @@ +use crate::syn_helpers::MemberExts; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use super::parsely_write_field_data::ParselyWriteFieldData; + +pub(crate) struct ParselyWriteVariantData { + pub(crate) enum_name: syn::Ident, + pub(crate) ident: syn::Ident, + pub(crate) discriminant: Option, + pub(crate) fields: Vec, +} + +impl ParselyWriteVariantData { + /// Returns true if this variant contains named fields, false otherwise + fn named_fields(&self) -> bool { + self.fields + .iter() + .any(|f| matches!(f.common.ident, syn::Member::Named(_))) + } +} + +impl ToTokens for ParselyWriteVariantData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let enum_name = &self.enum_name; + let variant_name = &self.ident; + + let body = if let Some(ref discriminant) = self.discriminant { + quote! { + #enum_name::#variant_name => { + let discriminant_value = #discriminant; + + discriminant_value.write::(buf, ()).context("Writing discriminant value of variant #variant_name") + } + } + } else if !self.fields.is_empty() { + let fields = &self.fields; + let field_variable_names = fields + .iter() + .map(|f| f.common.ident.as_variable_name()) + .collect::>(); + if self.named_fields() { + quote! { + #enum_name::#variant_name { #(ref #field_variable_names,)* } => { + #(#fields)* + } + } + } else { + quote! { + #enum_name::#variant_name(#(ref #field_variable_names,)*) => { + #(#fields)* + } + } + } + } else { + quote! { + #enum_name::#variant_name => {} + } + }; + + tokens.extend(body); + } +} diff --git a/impl/src/lib.rs b/impl/src/lib.rs index f3452fc..036b2d0 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -3,7 +3,7 @@ pub mod error; mod model_types; pub mod parsely_read; pub mod parsely_write; -mod syn_helpers; +pub(crate) mod syn_helpers; pub use bits_io::{ buf::bit_buf::BitBuf, @@ -30,7 +30,10 @@ use code_gen::{ parsely_read_enum_data::ParselyReadEnumData, parsely_read_struct_data::ParselyReadStructData, }, - write::parsely_write_struct_data::ParselyWriteStructData, + write::{ + parsely_write_enum_data::ParselyWriteEnumData, + parsely_write_struct_data::ParselyWriteStructData, + }, }; use darling::{ast, FromDeriveInput, FromField, FromMeta, FromVariant}; use model_types::{Assertion, Context, ExprOrFunc, MapExpr, TypedFnArgList}; @@ -70,7 +73,10 @@ pub fn derive_parsely_write(item: TokenStream) -> std::result::Result, + fields: ast::Fields, +} + #[derive(Debug, FromDeriveInput)] #[darling(attributes(parsely, parsely_read), supports(struct_any, enum_any))] pub struct ParselyReadReceiver { ident: syn::Ident, - required_context: Option, + #[darling(default)] + required_context: TypedFnArgList, alignment: Option, // Enums require a value to match on to determine which variant should be parsed key: Option, @@ -162,10 +177,12 @@ pub struct ParselyReadReceiver { #[darling(attributes(parsely, parsely_write), supports(struct_any, enum_any))] pub struct ParselyWriteReceiver { ident: syn::Ident, - required_context: Option, - sync_args: Option, + #[darling(default)] + required_context: TypedFnArgList, + #[darling(default)] + sync_args: TypedFnArgList, alignment: Option, - data: ast::Data<(), ParselyWriteFieldReceiver>, + data: ast::Data, } pub(crate) fn get_crate_name() -> syn::Ident { diff --git a/impl/src/model_types.rs b/impl/src/model_types.rs index f6d08c4..a1c2810 100644 --- a/impl/src/model_types.rs +++ b/impl/src/model_types.rs @@ -3,7 +3,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use syn::parse::Parse; -use crate::get_crate_name; +use crate::{get_crate_name, syn_helpers::MemberExts}; #[derive(Debug)] pub(crate) enum CollectionLimit { @@ -11,7 +11,7 @@ pub(crate) enum CollectionLimit { While(syn::Expr), } -#[derive(Debug)] +#[derive(Debug, Default)] pub(crate) struct TypedFnArgList(pub(crate) Vec); impl TypedFnArgList { @@ -239,7 +239,7 @@ impl FromMeta for MapExpr { } impl MapExpr { - pub(crate) fn to_read_map_tokens(&self, field_name: &MemberIdent, tokens: &mut TokenStream) { + pub(crate) fn to_read_map_tokens(&self, field_name: &syn::Member, tokens: &mut TokenStream) { let crate_name = get_crate_name(); let field_name_string = field_name.as_friendly_string(); let map_expr = &self.0; @@ -255,13 +255,13 @@ impl MapExpr { }) } - pub(crate) fn to_write_map_tokens(&self, field_name: &syn::Ident, tokens: &mut TokenStream) { + pub(crate) fn to_write_map_tokens(&self, field_ident: &syn::Member, tokens: &mut TokenStream) { let crate_name = get_crate_name(); - let field_name_string = field_name.to_string(); + let field_name_string = field_ident.as_friendly_string(); let map_expr = &self.0; tokens.extend(quote! { { - let mapped_value = (#map_expr)(&self.#field_name); + let mapped_value = (#map_expr)(&self.#field_ident); // Coerce the result of the mapping function into a ParselyResult where we know // T is writable to the buffer. We need to use this syntax because otherwise the // compiler gets caught up on trying to infer the buffer type. @@ -305,66 +305,21 @@ impl Assertion { }); } - pub(crate) fn to_write_assertion_tokens(&self, field_name: &str, tokens: &mut TokenStream) { + pub(crate) fn to_write_assertion_tokens( + &self, + field_ident: &syn::Member, + tokens: &mut TokenStream, + ) { let assertion = &self.0; let assertion_string = quote! { #assertion }.to_string(); - let assertion_func_ident = format_ident!("__{}_assertion_func", field_name); - let field_name_ident = format_ident!("{field_name}"); + let assertion_func_ident = + format_ident!("__{}_assertion_func", field_ident.as_variable_name()); + let field_name_str = field_ident.as_friendly_string(); tokens.extend(quote! { let #assertion_func_ident = #assertion; - if !#assertion_func_ident(&self.#field_name_ident) { - bail!("Assertion failed: value of field '{}' ('{:?}') didn't pass assertion: '{}'", #field_name, self.#field_name_ident, #assertion_string) + if !#assertion_func_ident(&self.#field_ident) { + bail!("Assertion failed: value of field '{}' ('{:?}') didn't pass assertion: '{}'", #field_name_str, self.#field_ident, #assertion_string) } }) } } - -#[derive(Debug)] -pub(crate) enum MemberIdent { - Named(syn::Ident), - // Unnamed members just have an index - Unnamed(u32), -} - -impl MemberIdent { - /// Create a `MemberIdent` from the given `ident`, if it's `Some` or the given `index` if not. - pub fn from_ident_or_index(ident: Option<&syn::Ident>, index: u32) -> Self { - if let Some(ident) = ident { - MemberIdent::Named(ident.to_owned()) - } else { - MemberIdent::Unnamed(index) - } - } - - pub fn from_ident(ident: &syn::Ident) -> Self { - MemberIdent::Named(ident.to_owned()) - } - - /// Return the value of this `MemberIdent` as a user-friendly String. This version is intended - /// to be used for things like error messages. - pub fn as_friendly_string(&self) -> String { - match self { - MemberIdent::Named(ident) => ident.to_string(), - MemberIdent::Unnamed(index) => format!("Field {index}"), - } - } - - /// Return the value of this `MemberIdent` in the form of a `syn::Ident` that can be used as a - /// local variable. - pub fn as_variable_name(&self) -> syn::Ident { - match self { - MemberIdent::Named(ident) => ident.clone(), - MemberIdent::Unnamed(index) => format_ident!("field_{index}"), - } - } - - /// Return the value of this `MemberIdent` in the form of a `syn::Ident` such that it can be - /// used to access this field inside the containing structure or enum. E.g. for a named - /// variable it will be the field's name, for an unnamed variable it will be the field's index. - pub fn field_name(&self) -> syn::Ident { - match self { - MemberIdent::Named(ident) => ident.clone(), - MemberIdent::Unnamed(index) => format_ident!("{index}"), - } - } -} diff --git a/impl/src/syn_helpers.rs b/impl/src/syn_helpers.rs index b403ad7..0181b67 100644 --- a/impl/src/syn_helpers.rs +++ b/impl/src/syn_helpers.rs @@ -1,3 +1,5 @@ +use quote::format_ident; + pub(crate) trait TypeExts { fn is_option(&self) -> bool; fn is_collection(&self) -> bool; @@ -59,3 +61,40 @@ impl TypeExts for syn::Type { Some(inner_type) } } + +pub(crate) trait MemberExts { + fn from_ident_or_index(ident: Option<&syn::Ident>, index: u32) -> Self; + /// Return the value of this `syn::Member` as a user-friendly String. This version is intended + /// to be used for things like error messages. + fn as_friendly_string(&self) -> String; + /// Return the value of this `syn::Member` in the form of a `syn::Ident` that can be used as a + /// local variable. + fn as_variable_name(&self) -> syn::Ident; +} + +impl MemberExts for syn::Member { + fn from_ident_or_index(ident: Option<&syn::Ident>, index: u32) -> Self { + if let Some(ident) = ident { + syn::Member::Named(ident.clone()) + } else { + syn::Member::Unnamed(syn::Index { + index, + span: proc_macro2::Span::call_site(), + }) + } + } + + fn as_friendly_string(&self) -> String { + match self { + syn::Member::Named(ref ident) => ident.to_string(), + syn::Member::Unnamed(ref index) => format!("Field {}", index.index), + } + } + + fn as_variable_name(&self) -> syn::Ident { + match self { + syn::Member::Named(ref ident) => ident.clone(), + syn::Member::Unnamed(ref index) => format_ident!("field_{}", index.index), + } + } +} diff --git a/tests/expand/alignment.expanded.rs b/tests/expand/alignment.expanded.rs index efb6bd6..c4a6d42 100644 --- a/tests/expand/alignment.expanded.rs +++ b/tests/expand/alignment.expanded.rs @@ -36,7 +36,7 @@ impl ::parsely_rs::ParselyWrite for Foo { Ok(()) } } -impl StateSync for Foo { +impl ::parsely_rs::StateSync for Foo { type SyncCtx = (); fn sync(&mut self, (): ()) -> ParselyResult<()> { self.one diff --git a/tests/expand/assertion.expanded.rs b/tests/expand/assertion.expanded.rs index a72c02e..84a88cc 100644 --- a/tests/expand/assertion.expanded.rs +++ b/tests/expand/assertion.expanded.rs @@ -62,7 +62,7 @@ impl ::parsely_rs::ParselyWrite for Foo { Ok(()) } } -impl StateSync for Foo { +impl ::parsely_rs::StateSync for Foo { type SyncCtx = (); fn sync(&mut self, (): ()) -> ParselyResult<()> { self.value diff --git a/tests/expand/map.expanded.rs b/tests/expand/map.expanded.rs index f892686..a22db23 100644 --- a/tests/expand/map.expanded.rs +++ b/tests/expand/map.expanded.rs @@ -57,7 +57,7 @@ impl ::parsely_rs::ParselyWrite for Foo { Ok(()) } } -impl StateSync for Foo { +impl ::parsely_rs::StateSync for Foo { type SyncCtx = (); fn sync(&mut self, (): ()) -> ParselyResult<()> { self.value diff --git a/tests/ui/pass/enum_basic.rs b/tests/ui/pass/enum_basic.rs index 7189306..d396b07 100644 --- a/tests/ui/pass/enum_basic.rs +++ b/tests/ui/pass/enum_basic.rs @@ -1,6 +1,6 @@ use parsely_rs::*; -#[derive(Debug, ParselyRead)] +#[derive(Debug, ParselyRead, ParselyWrite)] #[parsely_read(key = "buf.get_u8().unwrap()")] enum Foo { #[parsely_read(id = 1)] @@ -30,4 +30,9 @@ fn main() { assert!(matches!(two, Foo::Two(1))); let three = Foo::read::(&mut bits, ()).expect("three"); assert!(matches!(three, Foo::Three { bar: 1, baz: 42 })); + + // TODO: write test: need to fix enum writer to always write tag + let mut bits_mut = BitsMut::new(); + one.write::(&mut bits_mut, ()) + .expect("successful write one"); } diff --git a/tests/ui/pass/newtype_struct.rs b/tests/ui/pass/newtype_struct.rs index 7f6af31..443f3d1 100644 --- a/tests/ui/pass/newtype_struct.rs +++ b/tests/ui/pass/newtype_struct.rs @@ -1,6 +1,6 @@ use parsely_rs::*; -#[derive(ParselyRead)] +#[derive(ParselyRead, ParselyWrite)] struct Foo(u8); fn main() { @@ -8,4 +8,10 @@ fn main() { let foo = Foo::read::(&mut bits, ()).expect("successful parse"); assert_eq!(foo.0, 42); + + let mut bits_mut = BitsMut::new(); + + foo.write::(&mut bits_mut, ()) + .expect("successful write"); + assert_eq!(bits_mut.chunk_bytes()[0], 42); } diff --git a/tests/ui/pass/tuple_struct.rs b/tests/ui/pass/tuple_struct.rs index 5f69bc0..2657149 100644 --- a/tests/ui/pass/tuple_struct.rs +++ b/tests/ui/pass/tuple_struct.rs @@ -1,6 +1,6 @@ use parsely_rs::*; -#[derive(ParselyRead)] +#[derive(ParselyRead, ParselyWrite)] struct Foo(u8, u8); fn main() { @@ -9,4 +9,11 @@ fn main() { let foo = Foo::read::(&mut bits, ()).expect("successful parse"); assert_eq!(foo.0, 42); assert_eq!(foo.1, 43); + + let mut bits_mut = BitsMut::new(); + foo.write::(&mut bits_mut, ()) + .expect("successful write"); + let bytes = bits_mut.chunk_bytes(); + assert_eq!(bytes[0], 42); + assert_eq!(bytes[1], 43); } From d2aca97d59feb50dc157b53092743458508b88d9 Mon Sep 17 00:00:00 2001 From: bbaldino Date: Tue, 3 Jun 2025 15:48:13 -0700 Subject: [PATCH 09/10] fix enum write --- Cargo.lock | 2 ++ .../code_gen/read/parsely_read_enum_data.rs | 14 ++++++++------ .../code_gen/write/parsely_write_enum_data.rs | 5 +++++ .../write/parsely_write_variant_data.rs | 19 +++++++++++++++++-- impl/src/lib.rs | 7 +++++-- tests/ui/pass/enum_basic.rs | 8 ++++---- 6 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 609905b..080d85d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,8 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "bits-io" version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4228f764442a055482d34ff0821a0d11d10d5f6972752f3208d99765b0dfbb" dependencies = [ "bitvec", "bytes", diff --git a/impl/src/code_gen/read/parsely_read_enum_data.rs b/impl/src/code_gen/read/parsely_read_enum_data.rs index 1e8f79b..099e61e 100644 --- a/impl/src/code_gen/read/parsely_read_enum_data.rs +++ b/impl/src/code_gen/read/parsely_read_enum_data.rs @@ -16,7 +16,7 @@ pub(crate) struct ParselyReadEnumData { pub(crate) ident: syn::Ident, pub(crate) required_context: TypedFnArgList, pub(crate) alignment: Option, - pub(crate) key: syn::Expr, + pub(crate) key_type: syn::Type, pub(crate) variants: Vec, } @@ -24,8 +24,8 @@ impl TryFrom for ParselyReadEnumData { type Error = anyhow::Error; fn try_from(value: ParselyReadReceiver) -> Result { - let key = value - .key + let key_type = value + .key_type .ok_or(anyhow!("'key' attribute is required on enums"))?; let variants = value .data @@ -57,7 +57,7 @@ impl TryFrom for ParselyReadEnumData { Ok(ParselyReadEnumData { ident: value.ident, - key, + key_type, required_context: value.required_context, alignment: value.alignment, variants, @@ -69,14 +69,16 @@ impl ToTokens for ParselyReadEnumData { fn to_tokens(&self, tokens: &mut TokenStream) { let crate_name = get_crate_name(); let enum_name = &self.ident; + let enum_name_string = enum_name.to_string(); let (context_variables, context_types) = (self.required_context.names(), self.required_context.types()); - let match_value = &self.key; + let match_type = &self.key_type; let match_arms = &self.variants; let body = quote! { - match #match_value { + let match_value = <#match_type as ::#crate_name::ParselyRead<_>>::read::(buf, ()).with_context(|| format!("Tag for enum '{}'", #enum_name_string))?; + match match_value { #(#match_arms)* _ => ParselyResult::<_>::Err(anyhow!("No arms matched value")), } diff --git a/impl/src/code_gen/write/parsely_write_enum_data.rs b/impl/src/code_gen/write/parsely_write_enum_data.rs index e552e67..23fab1d 100644 --- a/impl/src/code_gen/write/parsely_write_enum_data.rs +++ b/impl/src/code_gen/write/parsely_write_enum_data.rs @@ -24,6 +24,9 @@ impl TryFrom for ParselyWriteEnumData { type Error = anyhow::Error; fn try_from(value: ParselyWriteReceiver) -> Result { + let key_type = value + .key_type + .ok_or(anyhow!("'key' attribute is required on enums"))?; let variants = value .data .take_enum() @@ -46,6 +49,8 @@ impl TryFrom for ParselyWriteEnumData { enum_name: value.ident.clone(), ident: v.ident, discriminant: v.discriminant, + id: v.id, + key_type: key_type.clone(), fields: data_fields, } }) diff --git a/impl/src/code_gen/write/parsely_write_variant_data.rs b/impl/src/code_gen/write/parsely_write_variant_data.rs index aed89e4..c608e66 100644 --- a/impl/src/code_gen/write/parsely_write_variant_data.rs +++ b/impl/src/code_gen/write/parsely_write_variant_data.rs @@ -1,4 +1,4 @@ -use crate::syn_helpers::MemberExts; +use crate::{get_crate_name, syn_helpers::MemberExts}; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; @@ -7,7 +7,9 @@ use super::parsely_write_field_data::ParselyWriteFieldData; pub(crate) struct ParselyWriteVariantData { pub(crate) enum_name: syn::Ident, pub(crate) ident: syn::Ident, + pub(crate) id: syn::Expr, pub(crate) discriminant: Option, + pub(crate) key_type: syn::Type, pub(crate) fields: Vec, } @@ -22,12 +24,21 @@ impl ParselyWriteVariantData { impl ToTokens for ParselyWriteVariantData { fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); let enum_name = &self.enum_name; let variant_name = &self.ident; + let tag_expr = &self.id; + let tag_type = &self.key_type; + let tag_write = quote! { + let tag_value: #tag_type = #tag_expr; + ::#crate_name::ParselyWrite::write::(&tag_value, buf, ())?; + }; + let body = if let Some(ref discriminant) = self.discriminant { quote! { #enum_name::#variant_name => { + #tag_write let discriminant_value = #discriminant; discriminant_value.write::(buf, ()).context("Writing discriminant value of variant #variant_name") @@ -42,19 +53,23 @@ impl ToTokens for ParselyWriteVariantData { if self.named_fields() { quote! { #enum_name::#variant_name { #(ref #field_variable_names,)* } => { + #tag_write #(#fields)* } } } else { quote! { #enum_name::#variant_name(#(ref #field_variable_names,)*) => { + #tag_write #(#fields)* } } } } else { quote! { - #enum_name::#variant_name => {} + #enum_name::#variant_name => { + #tag_write + } } }; diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 036b2d0..1a725d0 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -158,6 +158,7 @@ pub struct ParselyWriteFieldReceiver { pub struct ParselyWriteVariantReceiver { ident: syn::Ident, discriminant: Option, + id: syn::Expr, fields: ast::Fields, } @@ -168,8 +169,8 @@ pub struct ParselyReadReceiver { #[darling(default)] required_context: TypedFnArgList, alignment: Option, - // Enums require a value to match on to determine which variant should be parsed - key: Option, + // Enums require a type to denote the tag type that determines which variant will be read + key_type: Option, data: ast::Data, } @@ -182,6 +183,8 @@ pub struct ParselyWriteReceiver { #[darling(default)] sync_args: TypedFnArgList, alignment: Option, + // Enums require a type to denote the tag type to be written to denote the variant + key_type: Option, data: ast::Data, } diff --git a/tests/ui/pass/enum_basic.rs b/tests/ui/pass/enum_basic.rs index d396b07..44114b4 100644 --- a/tests/ui/pass/enum_basic.rs +++ b/tests/ui/pass/enum_basic.rs @@ -1,13 +1,13 @@ use parsely_rs::*; #[derive(Debug, ParselyRead, ParselyWrite)] -#[parsely_read(key = "buf.get_u8().unwrap()")] +#[parsely(key_type = "u8")] enum Foo { - #[parsely_read(id = 1)] + #[parsely(id = 1)] One, - #[parsely_read(id = 2)] + #[parsely(id = 2)] Two(u8), - #[parsely_read(id = 3)] + #[parsely(id = 3)] Three { bar: u8, baz: u16 }, } From 24a9a1764154062d402e56e497da4f95d37222b1 Mon Sep 17 00:00:00 2001 From: bbaldino Date: Tue, 3 Jun 2025 16:37:23 -0700 Subject: [PATCH 10/10] add enum write unit test --- .../code_gen/read/parsely_read_enum_data.rs | 2 +- .../code_gen/write/parsely_write_enum_data.rs | 2 +- tests/expand/enum_basic.expanded.rs | 132 ++++++++++++++++++ tests/expand/enum_basic.rs | 12 ++ tests/ui/pass/enum_basic.rs | 9 +- 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 tests/expand/enum_basic.expanded.rs create mode 100644 tests/expand/enum_basic.rs diff --git a/impl/src/code_gen/read/parsely_read_enum_data.rs b/impl/src/code_gen/read/parsely_read_enum_data.rs index 099e61e..f5baf97 100644 --- a/impl/src/code_gen/read/parsely_read_enum_data.rs +++ b/impl/src/code_gen/read/parsely_read_enum_data.rs @@ -26,7 +26,7 @@ impl TryFrom for ParselyReadEnumData { fn try_from(value: ParselyReadReceiver) -> Result { let key_type = value .key_type - .ok_or(anyhow!("'key' attribute is required on enums"))?; + .ok_or(anyhow!("'key_type' attribute is required on enums"))?; let variants = value .data .take_enum() diff --git a/impl/src/code_gen/write/parsely_write_enum_data.rs b/impl/src/code_gen/write/parsely_write_enum_data.rs index 23fab1d..32de476 100644 --- a/impl/src/code_gen/write/parsely_write_enum_data.rs +++ b/impl/src/code_gen/write/parsely_write_enum_data.rs @@ -26,7 +26,7 @@ impl TryFrom for ParselyWriteEnumData { fn try_from(value: ParselyWriteReceiver) -> Result { let key_type = value .key_type - .ok_or(anyhow!("'key' attribute is required on enums"))?; + .ok_or(anyhow!("'key_type' attribute is required on enums"))?; let variants = value .data .take_enum() diff --git a/tests/expand/enum_basic.expanded.rs b/tests/expand/enum_basic.expanded.rs new file mode 100644 index 0000000..8b44df3 --- /dev/null +++ b/tests/expand/enum_basic.expanded.rs @@ -0,0 +1,132 @@ +use parsely_rs::*; +#[parsely(key_type = "u8")] +enum Foo { + #[parsely(id = 1)] + One, + #[parsely(id = 2)] + Two(u8), + #[parsely(id = 3)] + Three { bar: u8, baz: u16 }, +} +#[automatically_derived] +impl ::core::fmt::Debug for Foo { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + Foo::One => ::core::fmt::Formatter::write_str(f, "One"), + Foo::Two(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Two", &__self_0) + } + Foo::Three { bar: __self_0, baz: __self_1 } => { + ::core::fmt::Formatter::debug_struct_field2_finish( + f, + "Three", + "bar", + __self_0, + "baz", + &__self_1, + ) + } + } + } +} +impl ::parsely_rs::ParselyRead for Foo { + type Ctx = (); + fn read( + buf: &mut B, + (): (), + ) -> ::parsely_rs::ParselyResult { + let match_value = >::read::(buf, ()) + .with_context(|| ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Tag for enum \'{0}\'", "Foo"), + ); + res + }))?; + match match_value { + 1 => Ok(Foo::One), + 2 => { + let field_0 = u8::read::(buf, ()) + .with_context(|| "Reading field 'Field 0'")?; + Ok(Foo::Two(field_0)) + } + 3 => { + let bar = u8::read::(buf, ()).with_context(|| "Reading field 'bar'")?; + let baz = u16::read::(buf, ()) + .with_context(|| "Reading field 'baz'")?; + Ok(Foo::Three { bar, baz }) + } + _ => { + ParselyResult::< + _, + >::Err( + ::anyhow::__private::must_use({ + let error = ::anyhow::__private::format_err( + format_args!("No arms matched value"), + ); + error + }), + ) + } + } + } +} +impl ::parsely_rs::ParselyWrite for Foo { + type Ctx = (); + fn write(&self, buf: &mut B, (): Self::Ctx) -> ParselyResult<()> { + match self { + Foo::One => { + let tag_value: u8 = 1; + ::parsely_rs::ParselyWrite::write::(&tag_value, buf, ())?; + } + Foo::Two(ref field_0) => { + let tag_value: u8 = 2; + ::parsely_rs::ParselyWrite::write::(&tag_value, buf, ())?; + u8::write::(&field_0, buf, ()) + .with_context(|| ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Writing field \'{0}\'", "Field 0"), + ); + res + }))?; + } + Foo::Three { ref bar, ref baz } => { + let tag_value: u8 = 3; + ::parsely_rs::ParselyWrite::write::(&tag_value, buf, ())?; + u8::write::(&bar, buf, ()) + .with_context(|| ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Writing field \'{0}\'", "bar"), + ); + res + }))?; + u16::write::(&baz, buf, ()) + .with_context(|| ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Writing field \'{0}\'", "baz"), + ); + res + }))?; + } + _ => { + ParselyResult::< + (), + >::Err( + ::anyhow::__private::must_use({ + let error = ::anyhow::__private::format_err( + format_args!("No arms matched self"), + ); + error + }), + )? + } + } + Ok(()) + } +} +impl ::parsely_rs::StateSync for Foo { + type SyncCtx = (); + fn sync(&mut self, (): ()) -> ParselyResult<()> { + Ok(()) + } +} diff --git a/tests/expand/enum_basic.rs b/tests/expand/enum_basic.rs new file mode 100644 index 0000000..23050f1 --- /dev/null +++ b/tests/expand/enum_basic.rs @@ -0,0 +1,12 @@ +use parsely_rs::*; + +#[derive(Debug, ParselyRead, ParselyWrite)] +#[parsely(key_type = "u8")] +enum Foo { + #[parsely(id = 1)] + One, + #[parsely(id = 2)] + Two(u8), + #[parsely(id = 3)] + Three { bar: u8, baz: u16 }, +} diff --git a/tests/ui/pass/enum_basic.rs b/tests/ui/pass/enum_basic.rs index 44114b4..f5edfc6 100644 --- a/tests/ui/pass/enum_basic.rs +++ b/tests/ui/pass/enum_basic.rs @@ -23,6 +23,7 @@ fn main() { 3, 1, 0, 42, ] ); + let bits_clone = bits.clone(); let one = Foo::read::(&mut bits, ()).expect("one"); assert!(matches!(one, Foo::One)); @@ -31,8 +32,14 @@ fn main() { let three = Foo::read::(&mut bits, ()).expect("three"); assert!(matches!(three, Foo::Three { bar: 1, baz: 42 })); - // TODO: write test: need to fix enum writer to always write tag let mut bits_mut = BitsMut::new(); one.write::(&mut bits_mut, ()) .expect("successful write one"); + two.write::(&mut bits_mut, ()) + .expect("successful write two"); + three + .write::(&mut bits_mut, ()) + .expect("successful write three"); + + assert_eq!(bits_clone, bits_mut.freeze()); }