Skip to main content

byteable_derive/
lib.rs

1use proc_macro_crate::{FoundCrate, crate_name};
2use proc_macro2::Span;
3use quote::{format_ident, quote};
4use syn::{Data, DeriveInput, Fields, Ident, Meta, Type, parse_macro_input};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7enum AttributeType {
8    LittleEndian,
9    BigEndian,
10    TryTransparent,
11    IoOnly,
12    None,
13}
14
15fn parse_byteable_attr(attrs: &[syn::Attribute]) -> AttributeType {
16    for attr in attrs {
17        if attr.path().is_ident("byteable") {
18            if let Meta::List(meta_list) = &attr.meta {
19                return match meta_list.tokens.to_string().as_str() {
20                    "little_endian" => AttributeType::LittleEndian,
21                    "big_endian" => AttributeType::BigEndian,
22                    "transparent" => AttributeType::None,
23                    "try_transparent" => AttributeType::TryTransparent,
24                    "io_only" => AttributeType::IoOnly,
25                    other => panic!(
26                        "Unknown byteable attribute: {other}. \
27                         Valid attributes are: little_endian, big_endian, try_transparent, io_only"
28                    ),
29                };
30            }
31            panic!(
32                "Unknown byteable attribute. \
33                 Valid attributes are: little_endian, big_endian, try_transparent, io_only"
34            );
35        }
36    }
37    AttributeType::None
38}
39
40/// Resolves the path to the `byteable` crate (handles renamed imports and in-crate use).
41fn byteable_crate_path() -> proc_macro2::TokenStream {
42    match crate_name("byteable").expect("byteable is present in `Cargo.toml`") {
43        FoundCrate::Itself => quote!(::byteable),
44        FoundCrate::Name(name) => {
45            let ident = Ident::new(&name, Span::call_site());
46            quote!(#ident)
47        }
48    }
49}
50
51/// Derive macro that generates byte-serialization impls for structs and enums.
52///
53/// `#[derive(Byteable)]` inspects the annotated type and generates one of two sets of
54/// traits depending on whether `#[byteable(io_only)]` is present:
55///
56/// - **Fixed-size** (default for structs): generates [`RawRepr`], [`FromRawRepr`] or
57///   [`TryFromRawRepr`], [`IntoByteArray`], and [`FromByteArray`] or [`TryFromByteArray`].
58///   A hidden `#[repr(C, packed)]` raw struct is created to hold the on-wire layout.
59///
60/// - **I/O streaming** (`#[byteable(io_only)]` on structs, always for field enums):
61///   generates [`Readable`] and [`Writable`], reading/writing fields sequentially.
62///
63/// - **Unit enums** (all variants are unit): generates [`TryFromRawRepr`],
64///   [`IntoByteArray`], and [`TryFromByteArray`] using an automatically-chosen
65///   discriminant integer type (`u8` → `u16` → `u32` → `u64` based on variant count).
66///
67/// [`RawRepr`]: byteable::RawRepr
68/// [`FromRawRepr`]: byteable::FromRawRepr
69/// [`TryFromRawRepr`]: byteable::TryFromRawRepr
70/// [`IntoByteArray`]: byteable::IntoByteArray
71/// [`FromByteArray`]: byteable::FromByteArray
72/// [`TryFromByteArray`]: byteable::TryFromByteArray
73/// [`Readable`]: byteable::Readable
74/// [`Writable`]: byteable::Writable
75///
76/// # Struct-level attributes
77///
78/// Place these on the struct or enum itself:
79///
80/// | Attribute | Effect |
81/// |-----------|--------|
82/// | `#[byteable(little_endian)]` | All multi-byte fields use little-endian representation |
83/// | `#[byteable(big_endian)]` | All multi-byte fields use big-endian representation |
84/// | `#[byteable(io_only)]` | Generate `Readable`/`Writable` instead of fixed-size traits |
85///
86/// # Field-level attributes
87///
88/// Place these on individual fields or enum variants:
89///
90/// | Attribute | Effect |
91/// |-----------|--------|
92/// | `#[byteable(little_endian)]` | This field uses little-endian (overrides struct-level) |
93/// | `#[byteable(big_endian)]` | This field uses big-endian (overrides struct-level) |
94/// | `#[byteable(try_transparent)]` | Field decode may fail; the struct impl becomes `TryFromRawRepr` |
95///
96/// # Examples
97///
98/// ## Basic fixed-size struct
99///
100/// ```rust
101/// use byteable::{Byteable, IntoByteArray, TryFromByteArray};
102///
103/// #[derive(Byteable)]
104/// struct Point {
105///     x: f32,
106///     y: f32,
107/// }
108///
109/// let p = Point { x: 1.0, y: 2.0 };
110/// let bytes = p.into_byte_array();
111/// let p2 = Point::try_from_byte_array(bytes).unwrap();
112/// assert_eq!(p.x, p2.x);
113/// ```
114///
115/// ## Mixed-endian struct
116///
117/// ```rust
118/// use byteable::Byteable;
119///
120/// #[derive(Byteable)]
121/// #[byteable(big_endian)]
122/// struct NetworkHeader {
123///     magic: u32,
124///     #[byteable(little_endian)]
125///     flags: u16,   // little-endian despite struct-level big_endian
126///     version: u8,  // single-byte, endian has no effect
127/// }
128/// ```
129///
130/// ## Dynamic struct with `io_only`
131///
132/// ```rust
133/// use byteable::{Byteable, Writable, Readable};
134/// use byteable::io::{WriteValue, ReadValue};
135///
136/// #[derive(Byteable)]
137/// #[byteable(io_only)]
138/// struct Message {
139///     id: u32,
140///     body: String,
141///     tags: Vec<String>,
142/// }
143///
144/// let msg = Message { id: 1, body: "hello".into(), tags: vec![] };
145/// let mut buf = Vec::new();
146/// buf.write_value(&msg).unwrap();
147/// let msg2 = std::io::Cursor::new(&buf).read_value::<Message>().unwrap();
148/// assert_eq!(msg.id, msg2.id);
149/// ```
150///
151/// ## Unit enum (auto-inferred repr)
152///
153/// ```rust
154/// use byteable::{Byteable, IntoByteArray, TryFromByteArray};
155///
156/// #[derive(Byteable, Debug, PartialEq)]
157/// enum Color {
158///     Red,
159///     Green,
160///     Blue,
161/// }
162///
163/// // Fits in u8 (3 variants), so wire size is 1 byte.
164/// assert_eq!(Color::BYTE_SIZE, 1);
165/// let bytes = Color::Green.into_byte_array();
166/// assert_eq!(Color::try_from_byte_array(bytes).unwrap(), Color::Green);
167/// ```
168///
169/// ## Field enum
170///
171/// ```rust
172/// use byteable::{Byteable, Readable, Writable};
173/// use byteable::io::{WriteValue, ReadValue};
174///
175/// #[derive(Byteable, Debug, PartialEq)]
176/// enum Shape {
177///     Circle { radius: f32 },
178///     Rect { width: f32, height: f32 },
179/// }
180///
181/// let s = Shape::Circle { radius: 3.0 };
182/// let mut buf = Vec::new();
183/// buf.write_value(&s).unwrap();
184/// let s2 = std::io::Cursor::new(&buf).read_value::<Shape>().unwrap();
185/// assert_eq!(s, s2);
186/// ```
187#[proc_macro_derive(Byteable, attributes(byteable))]
188pub fn byteable_derive_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
189    let input: DeriveInput = parse_macro_input!(input);
190    match input.data {
191        Data::Struct(_) => return struct_derive(input),
192        Data::Enum(_) => return enum_derive(input),
193        Data::Union(_) => panic!("union structs are unsupported"),
194    }
195}
196
197fn struct_derive(input: DeriveInput) -> proc_macro::TokenStream {
198    if parse_byteable_attr(&input.attrs) == AttributeType::IoOnly {
199        return io_struct_derive(input);
200    }
201    fixed_struct_derived(input)
202}
203
204fn gen_struct_field_write(
205    field_access: &proc_macro2::TokenStream,
206    field_type: &Type,
207    attrs: &[syn::Attribute],
208    bc: &proc_macro2::TokenStream,
209) -> proc_macro2::TokenStream {
210    match parse_byteable_attr(attrs) {
211        AttributeType::LittleEndian => quote! {
212            writer.write_value(&<#field_type as #bc::HasEndianRepr>::to_little_endian(#field_access))?;
213        },
214        AttributeType::BigEndian => quote! {
215            writer.write_value(&<#field_type as #bc::HasEndianRepr>::to_big_endian(#field_access))?;
216        },
217        AttributeType::None => quote! { writer.write_value(&#field_access)?; },
218        AttributeType::IoOnly => {
219            panic!("#[byteable(io_only)] is a struct-level attribute and cannot be used on a field")
220        }
221        AttributeType::TryTransparent => panic!(
222            "#[byteable(try_transparent)] is not applicable in \
223             io_only mode; remove the annotation or use a plain field"
224        ),
225    }
226}
227
228fn gen_field_read(
229    field_ident: &Ident,
230    field_ty: &syn::Type,
231    attrs: &[syn::Attribute],
232    bc: &proc_macro2::TokenStream,
233) -> proc_macro2::TokenStream {
234    match parse_byteable_attr(attrs) {
235        AttributeType::LittleEndian => {
236            quote! { let #field_ident: #field_ty = reader.read_value::<<#field_ty as #bc::HasEndianRepr>::LE>()?.get(); }
237        }
238        AttributeType::BigEndian => {
239            quote! { let #field_ident: #field_ty = reader.read_value::<<#field_ty as #bc::HasEndianRepr>::BE>()?.get(); }
240        }
241        AttributeType::None => quote! { let #field_ident: #field_ty = reader.read_value()?; },
242        other => panic!(
243            "unsupported #[byteable] attribute `{other:?}` on field `{field_ident}`; \
244             only little_endian and big_endian are supported here"
245        ),
246    }
247}
248
249fn io_struct_derive(input: DeriveInput) -> proc_macro::TokenStream {
250    let bc = byteable_crate_path();
251    let name = &input.ident;
252
253    let fields_data = match &input.data {
254        Data::Struct(data) => &data.fields,
255        _ => unreachable!(),
256    };
257
258    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
259
260    if let Fields::Unit = fields_data {
261        let vis = &input.vis;
262        let raw_name = format_ident!("__byteable_raw_{}", name);
263        return quote! {
264            #[derive(Clone, Copy)]
265            #[doc(hidden)]
266            #[allow(non_camel_case_types)]
267            #vis struct #raw_name;
268
269            unsafe impl #bc::PlainOldData for #raw_name {}
270
271            impl #bc::RawRepr for #name {
272                type Raw = #raw_name;
273
274                #[inline]
275                fn to_raw(&self) -> #raw_name {
276                    #raw_name
277                }
278            }
279
280            impl #bc::FromRawRepr for #name {
281                #[inline]
282                fn from_raw(value: #raw_name) -> Self {
283                    Self
284                }
285            }
286
287            impl #bc::TryFromRawRepr for #name {
288                #[inline]
289                fn try_from_raw(value: #raw_name) -> Result<Self, #bc::DecodeError> {
290                    Ok(Self)
291                }
292            }
293
294            impl #bc::IntoByteArray for #raw_name
295                where #raw_name : #bc::PlainOldData
296            {
297                type ByteArray = [u8; ::core::mem::size_of::<Self>()];
298                fn into_byte_array(&self) -> Self::ByteArray {
299                    #[allow(unnecessary_transmutes)]
300                    unsafe { ::core::mem::transmute(*self) }
301                }
302            }
303
304            impl #bc::FromByteArray for #raw_name
305                where #raw_name : #bc::PlainOldData
306            {
307                fn from_byte_array(byte_array: <Self as #bc::IntoByteArray>::ByteArray) -> Self {
308                    #[allow(unnecessary_transmutes)]
309                    unsafe { ::core::mem::transmute(byte_array) }
310
311                }
312            }
313
314            impl #bc::IntoByteArray for #name
315            where
316                #name: #bc::RawRepr,
317                <#name as #bc::RawRepr>::Raw: #bc::IntoByteArray,
318            {
319                type ByteArray = <<Self as #bc::RawRepr>::Raw as #bc::IntoByteArray>::ByteArray;
320
321                fn into_byte_array(&self) -> Self::ByteArray {
322                    <Self as #bc::RawRepr>::to_raw(self).into_byte_array()
323                }
324            }
325
326            impl #bc::FromByteArray for #name
327            where
328                #name: #bc::FromRawRepr,
329                <#name as #bc::RawRepr>::Raw: #bc::FromByteArray,
330            {
331                fn from_byte_array(byte_array: Self::ByteArray) -> Self {
332                    let raw = <<Self as #bc::RawRepr>::Raw as #bc::FromByteArray>::from_byte_array(byte_array);
333                    <Self as #bc::FromRawRepr>::from_raw(raw)
334                }
335            }
336        }
337        .into();
338    }
339
340    let (fields, is_tuple) = match fields_data {
341        syn::Fields::Named(f) => (&f.named, false),
342        syn::Fields::Unnamed(f) => (&f.unnamed, true),
343        syn::Fields::Unit => unreachable!(),
344    };
345
346    let write_stmts: Vec<_> = fields
347        .iter()
348        .enumerate()
349        .map(|(i, field)| {
350            let field_access = if is_tuple {
351                let idx = syn::Index::from(i);
352                quote! { self.#idx }
353            } else {
354                let fname = field.ident.as_ref().unwrap();
355                quote! { self.#fname }
356            };
357            gen_struct_field_write(&field_access, &field.ty, &field.attrs, &bc)
358        })
359        .collect();
360
361    let (read_bindings, construct_expr): (Vec<_>, proc_macro2::TokenStream) = if is_tuple {
362        let idents: Vec<_> = (0..fields.len())
363            .map(|i| syn::Ident::new(&format!("__field_{i}"), name.span()))
364            .collect();
365        let bindings = fields
366            .iter()
367            .zip(&idents)
368            .map(|(f, id)| gen_field_read(id, &f.ty, &f.attrs, &bc))
369            .collect();
370        (bindings, quote! { Ok(Self(#(#idents),*)) })
371    } else {
372        let field_idents: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect();
373        let bindings = fields
374            .iter()
375            .map(|f| gen_field_read(f.ident.as_ref().unwrap(), &f.ty, &f.attrs, &bc))
376            .collect();
377        (bindings, quote! { Ok(Self { #(#field_idents),* }) })
378    };
379
380    quote! {
381        impl #impl_generics #bc::Readable for #name #type_generics #where_clause {
382            fn read_from(mut reader: &mut (impl ::std::io::Read + ?Sized)) -> Result<Self, #bc::ReadableError> {
383                use #bc::ReadValue;
384                #( #read_bindings )*
385                #construct_expr
386            }
387        }
388        impl #impl_generics #bc::Writable for #name #type_generics #where_clause {
389            fn write_to(&self, mut writer: &mut (impl ::std::io::Write + ?Sized)) -> ::std::io::Result<()> {
390                use #bc::WriteValue;
391                #( #write_stmts )*
392                Ok(())
393            }
394        }
395    }.into()
396}
397
398fn fixed_struct_derived(input: DeriveInput) -> proc_macro::TokenStream {
399    let bc = byteable_crate_path();
400    let original_name = &input.ident;
401
402    // let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
403
404    let fields_data = match &input.data {
405        Data::Struct(data) => &data.fields,
406        _ => unreachable!(),
407    };
408
409    let vis = &input.vis;
410    let raw_name = format_ident!("__byteable_raw_{}", original_name);
411
412    if let Fields::Unit = fields_data {
413        return quote! {
414            #[derive(Clone, Copy)]
415            #[doc(hidden)]
416            #[allow(non_camel_case_types)]
417            #vis struct #raw_name;
418
419            unsafe impl #bc::PlainOldData for #raw_name {}
420
421            impl #bc::RawRepr for #original_name {
422                type Raw = #raw_name;
423
424                #[inline]
425                fn to_raw(&self) -> #raw_name {
426                    #raw_name
427                }
428            }
429
430            impl #bc::FromRawRepr for #original_name {
431                #[inline]
432                fn from_raw(value: #raw_name) -> Self {
433                    Self
434                }
435            }
436
437            impl #bc::TryFromRawRepr for #original_name {
438                #[inline]
439                fn try_from_raw(value: #raw_name) -> Result<Self, #bc::DecodeError> {
440                    Ok(Self)
441                }
442            }
443
444            impl #bc::IntoByteArray for #raw_name
445                where #raw_name : #bc::PlainOldData
446            {
447                type ByteArray = [u8; ::core::mem::size_of::<Self>()];
448                fn into_byte_array(&self) -> Self::ByteArray {
449                    #[allow(unnecessary_transmutes)]
450                    unsafe { ::core::mem::transmute(*self) }
451                }
452            }
453
454            impl #bc::FromByteArray for #raw_name
455                where #raw_name : #bc::PlainOldData
456            {
457                fn from_byte_array(byte_array: <Self as #bc::IntoByteArray>::ByteArray) -> Self {
458                    #[allow(unnecessary_transmutes)]
459                    unsafe { ::core::mem::transmute(byte_array) }
460
461                }
462            }
463
464            impl #bc::IntoByteArray for #original_name
465            where
466                #original_name: #bc::RawRepr,
467                <#original_name as #bc::RawRepr>::Raw: #bc::IntoByteArray,
468            {
469                type ByteArray = [u8; ::core::mem::size_of::<<Self as #bc::RawRepr>::Raw>()];
470                fn into_byte_array(&self) -> Self::ByteArray {
471                    <Self as #bc::RawRepr>::to_raw(self).into_byte_array()
472                }
473            }
474
475            impl #bc::FromByteArray for #original_name
476            where
477                #original_name: #bc::FromRawRepr,
478                <#original_name as #bc::RawRepr>::Raw: #bc::FromByteArray,
479            {
480                fn from_byte_array(byte_array: Self::ByteArray) -> Self {
481                    let raw = <<Self as #bc::RawRepr>::Raw as #bc::FromByteArray>::from_byte_array(byte_array);
482                    <Self as #bc::FromRawRepr>::from_raw(raw)
483                }
484            }
485        }
486        .into();
487    }
488
489    let (fields, is_tuple) = match fields_data {
490        Fields::Named(f) => (&f.named, false),
491        Fields::Unnamed(f) => (&f.unnamed, true),
492        Fields::Unit => unreachable!(),
493    };
494
495    struct FieldInfo {
496        raw_field_def: proc_macro2::TokenStream,
497        to_raw_expr: proc_macro2::TokenStream,
498        from_raw_expr: proc_macro2::TokenStream,
499    }
500
501    // Process each field: determine raw type and to/from conversion expressions
502    let mut field_infos = Vec::new();
503    let mut has_try = false;
504
505    for (i, field) in fields.iter().enumerate() {
506        let field_type = &field.ty;
507        let attr = parse_byteable_attr(&field.attrs);
508        if attr == AttributeType::TryTransparent {
509            has_try = true;
510        }
511
512        let field_info = if is_tuple {
513            let idx = syn::Index::from(i);
514            match attr {
515                AttributeType::LittleEndian => FieldInfo {
516                    raw_field_def: quote! { #vis <#field_type as #bc::HasEndianRepr>::LE },
517                    to_raw_expr: quote! { <#field_type as #bc::HasEndianRepr>::to_little_endian(self.#idx) },
518                    from_raw_expr: quote! { <#field_type as #bc::FromEndianRepr>::from_little_endian(value.#idx) },
519                },
520                AttributeType::BigEndian => FieldInfo {
521                    raw_field_def: quote! { #vis <#field_type as #bc::HasEndianRepr>::BE },
522                    to_raw_expr: quote! { <#field_type as #bc::HasEndianRepr>::to_big_endian(self.#idx) },
523                    from_raw_expr: quote! { <#field_type as #bc::FromEndianRepr>::from_big_endian(value.#idx) },
524                },
525                AttributeType::TryTransparent => FieldInfo {
526                    raw_field_def: quote! { #vis <#field_type as #bc::RawRepr>::Raw },
527                    to_raw_expr: quote! { <#field_type as #bc::RawRepr>::to_raw(&self.#idx) },
528                    from_raw_expr: quote! { <#field_type as #bc::TryFromRawRepr>::try_from_raw(value.#idx)? },
529                },
530                AttributeType::IoOnly => panic!(
531                    "#[byteable(io_only)] is a struct-level attribute and cannot be used on individual fields"
532                ),
533                AttributeType::None => FieldInfo {
534                    raw_field_def: quote! { #vis <#field_type as #bc::RawRepr>::Raw },
535                    to_raw_expr: quote! { <#field_type as #bc::RawRepr>::to_raw(&self.#idx) },
536                    from_raw_expr: quote! { <#field_type as #bc::FromRawRepr>::from_raw(value.#idx) },
537                },
538            }
539        } else {
540            let name = field.ident.as_ref().unwrap();
541            match attr {
542                AttributeType::LittleEndian => FieldInfo {
543                    raw_field_def: quote! { #vis #name: <#field_type as #bc::HasEndianRepr>::LE },
544                    to_raw_expr: quote! { #name: <#field_type as #bc::HasEndianRepr>::to_little_endian(self.#name) },
545                    from_raw_expr: quote! { #name: <#field_type as #bc::FromEndianRepr>::from_little_endian(value.#name) },
546                },
547                AttributeType::BigEndian => FieldInfo {
548                    raw_field_def: quote! { #vis #name: <#field_type as #bc::HasEndianRepr>::BE },
549                    to_raw_expr: quote! { #name: <#field_type as #bc::HasEndianRepr>::to_big_endian(self.#name) },
550                    from_raw_expr: quote! { #name: <#field_type as #bc::FromEndianRepr>::from_big_endian(value.#name) },
551                },
552                AttributeType::TryTransparent => FieldInfo {
553                    raw_field_def: quote! { #vis #name: <#field_type as #bc::RawRepr>::Raw },
554                    to_raw_expr: quote! { #name: <#field_type as #bc::RawRepr>::to_raw(&self.#name) },
555                    from_raw_expr: quote! { #name: <#field_type as #bc::TryFromRawRepr>::try_from_raw(value.#name)? },
556                },
557                AttributeType::IoOnly => panic!(
558                    "#[byteable(io_only)] is a struct-level attribute and cannot be used on individual fields"
559                ),
560                AttributeType::None => FieldInfo {
561                    raw_field_def: quote! { #vis #name: <#field_type as #bc::RawRepr>::Raw },
562                    to_raw_expr: quote! { #name: <#field_type as #bc::RawRepr>::to_raw(&self.#name) },
563                    from_raw_expr: quote! { #name: <#field_type as #bc::FromRawRepr>::from_raw(value.#name) },
564                },
565            }
566        };
567        field_infos.push(field_info);
568    }
569
570    let raw_struct_def = {
571        let field_defs = field_infos.iter().map(|v| &v.raw_field_def);
572        if is_tuple {
573            quote! {
574                #[derive(Clone, Copy)]
575                #[repr(C, packed)]
576                #[doc(hidden)]
577                #[allow(non_camel_case_types)]
578                #vis struct #raw_name( #(#field_defs),* );
579            }
580        } else {
581            quote! {
582                #[derive(Clone, Copy)]
583                #[repr(C, packed)]
584                #[doc(hidden)]
585                #[allow(non_camel_case_types)]
586                #vis struct #raw_name { #(#field_defs),* }
587            }
588        }
589    };
590
591    let raw_impls = {
592        quote! {
593            unsafe impl #bc::PlainOldData for #raw_name {}
594
595            impl #bc::IntoByteArray for #raw_name
596                where #raw_name : #bc::PlainOldData
597            {
598                type ByteArray = [u8; ::core::mem::size_of::<Self>()];
599                fn into_byte_array(&self) -> Self::ByteArray {
600                    #[allow(unnecessary_transmutes)]
601                    unsafe { ::core::mem::transmute(*self) }
602                }
603            }
604
605            impl #bc::FromByteArray for #raw_name
606                where #raw_name : #bc::PlainOldData
607            {
608                fn from_byte_array(byte_array: <Self as #bc::IntoByteArray>::ByteArray) -> Self {
609                    #[allow(unnecessary_transmutes)]
610                    unsafe { ::core::mem::transmute(byte_array) }
611
612                }
613            }
614        }
615    };
616
617    let from_raw_body = {
618        let from_raw_exprs = field_infos.iter().map(|v| &v.from_raw_expr);
619        if is_tuple {
620            quote! { Self(#(#from_raw_exprs),*) }
621        } else {
622            quote! { Self { #(#from_raw_exprs),* } }
623        }
624    };
625
626    let raw_repr = {
627        let to_raw_exprs = field_infos.iter().map(|v| &v.to_raw_expr);
628        if is_tuple {
629            quote! {
630                impl #bc::RawRepr for #original_name {
631                    type Raw = #raw_name;
632
633                    #[inline]
634                    fn to_raw(&self) -> Self::Raw {
635                        #raw_name (#(#to_raw_exprs),*)
636                    }
637                }
638
639                impl #bc::IntoByteArray for #original_name
640                where
641                    #original_name: #bc::RawRepr,
642                    <#original_name as #bc::RawRepr>::Raw: #bc::IntoByteArray,
643                {
644                    type ByteArray = <<Self as #bc::RawRepr>::Raw as #bc::IntoByteArray>::ByteArray;
645                    fn into_byte_array(&self) -> Self::ByteArray {
646                        <Self as #bc::RawRepr>::to_raw(self).into_byte_array()
647                    }
648                }
649
650            }
651        } else {
652            quote! {
653                impl #bc::RawRepr for #original_name {
654                    type Raw = #raw_name;
655
656                    #[inline]
657                    fn to_raw(&self) -> Self::Raw {
658                        #raw_name { #(#to_raw_exprs),* }
659                    }
660                }
661
662                impl #bc::IntoByteArray for #original_name
663                where
664                    #original_name: #bc::RawRepr,
665                    <#original_name as #bc::RawRepr>::Raw: #bc::IntoByteArray,
666                {
667                    type ByteArray = <<Self as #bc::RawRepr>::Raw as #bc::IntoByteArray>::ByteArray;
668                    fn into_byte_array(&self) -> Self::ByteArray {
669                        <Self as #bc::RawRepr>::to_raw(self).into_byte_array()
670                    }
671                }
672            }
673        }
674    };
675
676    let original_impls = if has_try {
677        quote! {
678            impl #bc::TryFromRawRepr for #original_name {
679                #[inline]
680                fn try_from_raw(value: #raw_name) -> Result<Self, #bc::DecodeError> { Ok(#from_raw_body) }
681            }
682
683            impl #bc::TryFromByteArray for #original_name
684            where
685                #original_name: #bc::TryFromRawRepr,
686                <#original_name as #bc::RawRepr>::Raw: #bc::FromByteArray,
687            {
688                fn try_from_byte_array(byte_array: Self::ByteArray) -> Result<Self, #bc::DecodeError> {
689                    let raw = <<Self as #bc::RawRepr>::Raw as #bc::FromByteArray>::from_byte_array(byte_array);
690                    <Self as #bc::TryFromRawRepr>::try_from_raw(raw)
691                }
692            }
693
694        }
695    } else {
696        quote! {
697            impl #bc::FromRawRepr for #original_name {
698                #[inline]
699                fn from_raw(value: #raw_name) -> Self { #from_raw_body }
700            }
701
702            impl #bc::TryFromRawRepr for #original_name {
703                #[inline]
704                fn try_from_raw(value: #raw_name) -> Result<Self, #bc::DecodeError> { Ok(<Self as #bc::FromRawRepr>::from_raw(value)) }
705            }
706
707
708            impl #bc::FromByteArray for #original_name
709            where
710                #original_name: #bc::FromRawRepr,
711                <#original_name as #bc::RawRepr>::Raw: #bc::FromByteArray,
712            {
713                fn from_byte_array(byte_array: Self::ByteArray) -> Self {
714                    let raw = <<Self as #bc::RawRepr>::Raw as #bc::FromByteArray>::from_byte_array(byte_array);
715                    <Self as #bc::FromRawRepr>::from_raw(raw)
716                }
717            }
718        }
719    };
720
721    quote! {
722        #raw_struct_def
723        #raw_impls
724        #raw_repr
725        #original_impls
726    }
727    .into()
728}
729
730fn extract_repr_type(attrs: &[syn::Attribute]) -> Option<syn::Ident> {
731    for attr in attrs {
732        if attr.path().is_ident("repr") {
733            if let Meta::List(meta_list) = &attr.meta {
734                if let Ok(ident) = syn::parse2::<syn::Ident>(meta_list.tokens.clone()) {
735                    if matches!(
736                        ident.to_string().as_str(),
737                        "u8" | "i8"
738                            | "u16"
739                            | "i16"
740                            | "u32"
741                            | "i32"
742                            | "u64"
743                            | "i64"
744                            | "u128"
745                            | "i128"
746                    ) {
747                        return Some(ident);
748                    }
749                }
750            }
751        }
752    }
753    None
754}
755
756fn gen_enum_field_write(
757    field_ident: &Ident,
758    field_type: &Type,
759    attrs: &[syn::Attribute],
760    bc: &proc_macro2::TokenStream,
761) -> proc_macro2::TokenStream {
762    match parse_byteable_attr(attrs) {
763        AttributeType::LittleEndian => quote! {
764            writer.write_value(&<#field_type as #bc::HasEndianRepr>::to_little_endian(*#field_ident))?;
765        },
766        AttributeType::BigEndian => quote! {
767            writer.write_value(&<#field_type as #bc::HasEndianRepr>::to_big_endian(*#field_ident))?;
768        },
769        AttributeType::None => quote! {
770            writer.write_value(#field_ident)?;
771        },
772        other => panic!(
773            "unsupported #[byteable] attribute `{other:?}` on field `{field_ident}`; \
774             only little_endian and big_endian are supported here"
775        ),
776    }
777}
778
779fn enum_derive(input: DeriveInput) -> proc_macro::TokenStream {
780    let Data::Enum(enum_data) = &input.data else {
781        unreachable!();
782    };
783    let has_field_variants = enum_data
784        .variants
785        .iter()
786        .any(|v| !matches!(v.fields, Fields::Unit));
787    if !has_field_variants {
788        return unit_enum_derive(input);
789    }
790    let name = input.ident;
791    let bc = byteable_crate_path();
792
793    // generate io_only variant
794
795    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
796
797    // Determine repr type — use explicit #[repr(...)] if present, otherwise auto-select.
798    let repr_ty = extract_repr_type(&input.attrs).unwrap_or_else(|| {
799        let n = enum_data.variants.len();
800        let ty_str = if n <= 256 {
801            "u8"
802        } else if n <= 65_536 {
803            "u16"
804        } else if n as u64 <= u32::MAX as u64 + 1 {
805            "u32"
806        } else {
807            "u64"
808        };
809        Ident::new(ty_str, name.span())
810    });
811
812    let endian_attr = parse_byteable_attr(&input.attrs);
813    let discriminants = compute_discriminants(&enum_data.variants);
814
815    let read_disc = match endian_attr {
816        AttributeType::LittleEndian => quote! {
817            let disc: #repr_ty = <#repr_ty as #bc::FromEndianRepr>::from_little_endian(
818                reader.read_value::<<#repr_ty as #bc::HasEndianRepr>::LE>()?
819            );
820        },
821        AttributeType::BigEndian => quote! {
822            let disc: #repr_ty = <#repr_ty as #bc::FromEndianRepr>::from_big_endian(
823                reader.read_value::<<#repr_ty as #bc::HasEndianRepr>::BE>()?
824            );
825        },
826        _ => quote! {
827            let disc: #repr_ty = reader.read_value()?;
828        },
829    };
830
831    let write_arms = enum_data
832        .variants
833        .iter()
834        .zip(&discriminants)
835        .map(|(variant, disc_tokens)| {
836            let variant_name = &variant.ident;
837            let write_disc = match endian_attr {
838                AttributeType::LittleEndian => quote! {
839                    let disc_val: #repr_ty = #disc_tokens;
840                    writer.write_value(&<#repr_ty as #bc::HasEndianRepr>::to_little_endian(disc_val))?;
841                },
842                AttributeType::BigEndian => quote! {
843                    let disc_val: #repr_ty = #disc_tokens;
844                    writer.write_value(&<#repr_ty as #bc::HasEndianRepr>::to_big_endian(disc_val))?;
845                },
846                _ => quote! {
847                    let disc_val: #repr_ty = #disc_tokens;
848                    writer.write_value(&disc_val)?;
849                },
850            };
851            match &variant.fields {
852                Fields::Unit => quote! {
853                    #name::#variant_name => { #write_disc }
854                },
855                Fields::Named(named) => {
856                    let field_names: Vec<_> = named
857                        .named
858                        .iter()
859                        .map(|f| f.ident.as_ref().unwrap())
860                        .collect();
861                    let field_writes: Vec<_> = named
862                        .named
863                        .iter()
864                        .map(|f| {
865                            gen_enum_field_write(f.ident.as_ref().unwrap(), &f.ty, &f.attrs, &bc)
866                        })
867                        .collect();
868                    quote! {
869                        #name::#variant_name { #(#field_names),* } => {
870                            #write_disc
871                            #( #field_writes )*
872                        }
873                    }
874                }
875                Fields::Unnamed(unnamed) => {
876                    let field_idents: Vec<_> = (0..unnamed.unnamed.len())
877                        .map(|i| Ident::new(&format!("__field_{i}"), name.span()))
878                        .collect();
879                    let field_writes: Vec<_> = unnamed
880                        .unnamed
881                        .iter()
882                        .zip(&field_idents)
883                        .map(|(f, ident)| gen_enum_field_write(ident, &f.ty, &f.attrs, &bc))
884                        .collect();
885                    quote! {
886                        #name::#variant_name(#(#field_idents),*) => {
887                            #write_disc
888                            #( #field_writes )*
889                        }
890                    }
891                }
892            }
893        });
894    let read_arms = enum_data
895        .variants
896        .iter()
897        .zip(&discriminants)
898        .map(|(variant, disc_tokens)| {
899            let variant_name = &variant.ident;
900
901            match &variant.fields {
902                Fields::Unit => quote! {
903                    #disc_tokens => Ok(#name::#variant_name),
904                },
905                Fields::Named(named) => {
906                    let field_idents: Vec<_> = named
907                        .named
908                        .iter()
909                        .map(|f| f.ident.as_ref().unwrap())
910                        .collect();
911                    let field_reads: Vec<_> = named
912                        .named
913                        .iter()
914                        .map(|f| gen_field_read(f.ident.as_ref().unwrap(), &f.ty, &f.attrs, &bc))
915                        .collect();
916                    quote! {
917                        #disc_tokens => {
918                            #( #field_reads )*
919                            Ok(#name::#variant_name { #(#field_idents),* })
920                        }
921                    }
922                }
923                Fields::Unnamed(unnamed) => {
924                    let field_idents: Vec<_> = (0..unnamed.unnamed.len())
925                        .map(|i| Ident::new(&format!("__field_{i}"), name.span()))
926                        .collect();
927                    let field_reads: Vec<_> = unnamed
928                        .unnamed
929                        .iter()
930                        .zip(&field_idents)
931                        .map(|(f, ident)| gen_field_read(ident, &f.ty, &f.attrs, &bc))
932                        .collect();
933                    quote! {
934                        #disc_tokens => {
935                            #( #field_reads )*
936                            Ok(#name::#variant_name(#(#field_idents),*))
937                        }
938                    }
939                }
940            }
941        });
942    quote! {
943        impl #impl_generics #bc::Writable for #name #type_generics #where_clause {
944            fn write_to(&self, mut writer: &mut (impl ::std::io::Write + ?Sized)) -> ::std::io::Result<()> {
945                use #bc::WriteValue;
946                match self {
947                    #(#write_arms)*
948                }
949                Ok(())
950            }
951        }
952
953
954        impl #impl_generics #bc::Readable for #name #type_generics #where_clause {
955            fn read_from(mut reader: &mut (impl ::std::io::Read + ?Sized)) -> Result<Self, #bc::ReadableError> {
956                use #bc::ReadValue;
957                #read_disc
958                match disc {
959                    #(#read_arms)*
960                    _ => Err(#bc::ReadableError::DecodeError(#bc::DecodeError::InvalidDiscriminant { raw: disc as u64, type_name: ::core::stringify!(#name) })),
961                }
962            }
963        }
964    }.into()
965}
966
967fn try_eval_int_expr(expr: &syn::Expr) -> Option<u128> {
968    match expr {
969        syn::Expr::Lit(el) => {
970            if let syn::Lit::Int(li) = &el.lit {
971                // base10_parse handles decimal literals and strips type suffixes
972                if let Ok(v) = li.base10_parse::<u128>() {
973                    return Some(v);
974                }
975                // For non-decimal (hex/bin/oct), parse from the token string
976                let s = li.to_string();
977                let (prefix, rest) =
978                    if let Some(r) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
979                        (16u32, r)
980                    } else if let Some(r) = s.strip_prefix("0b").or_else(|| s.strip_prefix("0B")) {
981                        (2, r)
982                    } else if let Some(r) = s.strip_prefix("0o").or_else(|| s.strip_prefix("0O")) {
983                        (8, r)
984                    } else {
985                        return None;
986                    };
987                // Strip type suffix and digit separators
988                let digits: String = rest
989                    .chars()
990                    .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
991                    .filter(|c| *c != '_' && !c.is_alphabetic())
992                    .collect();
993                u128::from_str_radix(&digits, prefix).ok()
994            } else {
995                None
996            }
997        }
998        _ => None,
999    }
1000}
1001
1002/// Computes discriminant token streams for every variant, auto-assigning values where absent.
1003///
1004/// Follows Rust's own rule: starts at `0`, increments by one after each variant. If a variant
1005/// has an explicit discriminant, that value is used and the counter resets to `explicit + 1`.
1006/// When the explicit value cannot be statically evaluated (e.g. a named constant), the counter
1007/// falls back to incrementing from the previous known position.
1008fn compute_discriminants(
1009    variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
1010) -> Vec<proc_macro2::TokenStream> {
1011    let mut next: u128 = 0;
1012    variants
1013        .iter()
1014        .map(|v| {
1015            if let Some((_, expr)) = &v.discriminant {
1016                // Try to evaluate to keep the counter accurate
1017                if let Some(val) = try_eval_int_expr(expr) {
1018                    next = val + 1;
1019                } else {
1020                    next += 1;
1021                }
1022                quote! { #expr }
1023            } else {
1024                let val = next;
1025                next += 1;
1026                let lit = proc_macro2::Literal::u128_unsuffixed(val);
1027                quote! { #lit }
1028            }
1029        })
1030        .collect()
1031}
1032
1033fn unit_enum_derive(input: DeriveInput) -> proc_macro::TokenStream {
1034    let bc = byteable_crate_path();
1035    let Data::Enum(enum_data) = &input.data else {
1036        unreachable!();
1037    };
1038    let enum_name = &input.ident;
1039
1040    let repr_ty = extract_repr_type(&input.attrs).unwrap_or_else(|| {
1041        let n = enum_data.variants.len();
1042        let ty_str = if n <= 256 {
1043            "u8"
1044        } else if n <= 65_536 {
1045            "u16"
1046        } else if n as u64 <= u32::MAX as u64 + 1 {
1047            "u32"
1048        } else {
1049            "u64"
1050        };
1051        Ident::new(ty_str, enum_name.span())
1052    });
1053
1054    let endian_attr = parse_byteable_attr(&input.attrs);
1055    let discriminants = compute_discriminants(&enum_data.variants);
1056
1057    let from_discriminant_arms =
1058        enum_data
1059            .variants
1060            .iter()
1061            .zip(&discriminants)
1062            .map(|(variant, disc)| {
1063                let variant_name = &variant.ident;
1064                quote! { #disc => Ok(#enum_name::#variant_name), }
1065            });
1066
1067    let into_byte_array_body = match endian_attr {
1068        AttributeType::LittleEndian => quote! {
1069            let v: #repr_ty = *self as _;
1070            <#repr_ty as #bc::HasEndianRepr>::to_little_endian(v).into_byte_array()
1071        },
1072        AttributeType::BigEndian => quote! {
1073            let v: #repr_ty = *self as _;
1074            <#repr_ty as #bc::HasEndianRepr>::to_big_endian(v).into_byte_array()
1075        },
1076        _ => quote! {
1077            let v: #repr_ty = *self as _;
1078            <#repr_ty as #bc::IntoByteArray>::into_byte_array(&v)
1079        },
1080    };
1081
1082    let try_from_byte_array_body = match endian_attr {
1083        AttributeType::LittleEndian => quote! {
1084            let le = <<#repr_ty as #bc::HasEndianRepr>::LE as #bc::FromByteArray>::from_byte_array(byte_array);
1085            let raw = <#repr_ty as #bc::FromEndianRepr>::from_little_endian(le);
1086            <Self as #bc::TryFromRawRepr>::try_from_raw(raw)
1087        },
1088        AttributeType::BigEndian => quote! {
1089            let be = <<#repr_ty as #bc::HasEndianRepr>::BE as #bc::FromByteArray>::from_byte_array(byte_array);
1090            let raw = <#repr_ty as #bc::FromEndianRepr>::from_big_endian(be);
1091            <Self as #bc::TryFromRawRepr>::try_from_raw(raw)
1092        },
1093        _ => quote! {
1094            let raw = <#repr_ty as #bc::FromByteArray>::from_byte_array(byte_array);
1095            <Self as #bc::TryFromRawRepr>::try_from_raw(raw)
1096        },
1097    };
1098
1099    quote! {
1100        impl #bc::RawRepr for #enum_name {
1101            type Raw = #repr_ty;
1102            fn to_raw(&self) -> #repr_ty {
1103                *self as _
1104            }
1105        }
1106
1107        impl #bc::TryFromRawRepr for #enum_name {
1108            fn try_from_raw(raw: Self::Raw) -> Result<Self, #bc::DecodeError> {
1109                match raw {
1110                    #(#from_discriminant_arms)*
1111                    _ => Err(#bc::DecodeError::InvalidDiscriminant { raw: raw as u64, type_name: ::core::stringify!(#enum_name) })
1112                }
1113            }
1114        }
1115
1116        impl #bc::IntoByteArray for #enum_name {
1117            type ByteArray = [u8; ::core::mem::size_of::<#repr_ty>()];
1118            fn into_byte_array(&self) -> Self::ByteArray {
1119                #into_byte_array_body
1120            }
1121        }
1122
1123        impl #bc::TryFromByteArray for #enum_name {
1124            fn try_from_byte_array(byte_array: Self::ByteArray) -> Result<Self, #bc::DecodeError> {
1125                #try_from_byte_array_body
1126            }
1127        }
1128
1129    }
1130    .into()
1131}