Skip to main content

photon_ring_derive/
lib.rs

1// Copyright 2026 Photon Ring Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Derive macros for `photon_ring::Pod` and `photon_ring::Message`.
5//!
6//! ## `Pod` derive
7//!
8//! ```ignore
9//! #[repr(C)]
10//! #[derive(photon_ring::Pod)]
11//! struct Quote {
12//!     price: f64,
13//!     volume: u32,
14//! }
15//! ```
16//!
17//! This generates compile-time assertions that every field implements `Pod`,
18//! plus `unsafe impl photon_ring::Pod for Quote {}`.
19//!
20//! **Note:** The macro does *not* add `#[repr(C)]` or `Clone`/`Copy` derives.
21//! You must add those yourself for the `Pod` contract to hold.
22//!
23//! ## `Message` derive
24//!
25//! ```ignore
26//! #[derive(photon_ring::Message)]
27//! struct Order {
28//!     price: f64,
29//!     qty: u32,
30//!     #[photon(as_enum)]
31//!     side: Side,        // any #[repr(u8)] enum — requires #[photon(as_enum)]
32//!     filled: bool,
33//!     tag: Option<u32>,
34//! }
35//! ```
36//!
37//! Generates a Pod-compatible wire struct (`OrderWire`), a `From<Order> for
38//! OrderWire`, and a back-conversion: a safe `From<OrderWire> for Order` for
39//! enum-free structs, or an `unsafe OrderWire::into_domain()` method when
40//! `#[photon(as_enum)]` fields are present. See [`derive_message`] for details.
41
42use proc_macro::TokenStream;
43use proc_macro2::Span;
44use quote::{format_ident, quote};
45use syn::{
46    parse_macro_input, Data, DeriveInput, Fields, GenericArgument, Meta, PathArguments, Type,
47};
48
49/// Derive `Pod` for a struct.
50///
51/// Requirements:
52/// - Must be a struct (not enum or union).
53/// - All fields must implement `Pod`.
54/// - The user must add `#[repr(C)]`, `Clone`, and `Copy` themselves;
55///   the macro only emits field assertions and `unsafe impl Pod`.
56///
57/// # Example
58///
59/// ```ignore
60/// #[repr(C)]
61/// #[derive(photon_ring::Pod)]
62/// struct Tick {
63///     price: f64,
64///     volume: u32,
65///     _pad: u32,
66/// }
67/// ```
68#[proc_macro_derive(Pod)]
69pub fn derive_pod(input: TokenStream) -> TokenStream {
70    let input = parse_macro_input!(input as DeriveInput);
71    let name = &input.ident;
72    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
73
74    // Verify #[repr(C)] is present
75    let has_repr_c = input.attrs.iter().any(|attr| {
76        if !attr.path().is_ident("repr") {
77            return false;
78        }
79        let mut found = false;
80        if let Meta::List(list) = &attr.meta {
81            let _ = list.parse_nested_meta(|nested| {
82                if nested.path.is_ident("C") {
83                    found = true;
84                }
85                Ok(())
86            });
87        }
88        found
89    });
90    if !has_repr_c {
91        return syn::Error::new_spanned(
92            &input.ident,
93            "Pod can only be derived for #[repr(C)] structs",
94        )
95        .to_compile_error()
96        .into();
97    }
98
99    // Only structs are supported
100    let fields = match &input.data {
101        Data::Struct(s) => match &s.fields {
102            Fields::Named(f) => f.named.iter().collect::<Vec<_>>(),
103            Fields::Unnamed(f) => f.unnamed.iter().collect::<Vec<_>>(),
104            Fields::Unit => vec![],
105        },
106        _ => {
107            return syn::Error::new_spanned(&input.ident, "Pod can only be derived for structs")
108                .to_compile_error()
109                .into();
110        }
111    };
112
113    // Generate compile-time assertions that every field is Pod
114    let field_assertions = fields.iter().map(|f| {
115        let ty = &f.ty;
116        quote! {
117            const _: () = {
118                fn _assert_pod<T: photon_ring::Pod>() {}
119                fn _check() { _assert_pod::<#ty>(); }
120            };
121        }
122    });
123
124    let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect();
125
126    let expanded = quote! {
127        // Compile-time field checks
128        #(#field_assertions)*
129
130        // `Pod` forbids padding: an implicit gap is uninitialised memory, and the
131        // `atomic-slots` payload copy reads the value as integer chunks. Proving
132        // the size equals the sum of the field sizes is exactly the no-padding
133        // condition, and it fails the build rather than the run.
134        const _: () = {
135            let sum = 0usize #( + core::mem::size_of::<#field_types>() )*;
136            assert!(
137                core::mem::size_of::<#name>() == sum,
138                "Pod cannot be derived for a type with padding: add explicit \
139                 padding fields, or reorder fields so none is inserted",
140            );
141        };
142
143        // Safety: every field is Pod (asserted above), the type is #[repr(C)],
144        // and it carries no padding, so every byte is initialised and every bit
145        // pattern is a valid value.
146        unsafe impl #impl_generics photon_ring::Pod for #name #ty_generics #where_clause {}
147    };
148
149    TokenStream::from(expanded)
150}
151
152// ---------------------------------------------------------------------------
153// Message derive
154// ---------------------------------------------------------------------------
155
156/// Classification of a field type for wire conversion.
157enum FieldKind {
158    /// Numeric or array — passes through unchanged.
159    Passthrough,
160    /// `bool` → `u8`.
161    Bool,
162    /// `usize` → `u64`.
163    Usize,
164    /// `isize` → `i64`.
165    Isize,
166    /// `Option<T>` for a supported inner type. The wire struct gets a
167    /// `X_value: <wire_ty>` field plus a `X_has: u8` presence flag. The two
168    /// conversion snippets carry the per-type detail: `to_value` maps the
169    /// unwrapped `v` to the wire integer, `from_value` maps the loaded `raw`
170    /// wire integer back to the inner type.
171    Option {
172        wire_ty: proc_macro2::TokenStream,
173        to_value: proc_macro2::TokenStream,
174        from_value: proc_macro2::TokenStream,
175        /// `usize`/`isize` inner types need the 64-bit-fit compile assertion.
176        is_usize_isize: bool,
177    },
178    /// A `#[repr(u8)]` enum, explicitly marked with `#[photon(as_enum)]` → `u8`.
179    Enum,
180    /// Unrecognized type — will emit a compile error.
181    Unsupported,
182    /// Unsupported `Option<T>` inner type — will emit a compile error.
183    UnsupportedOption(String),
184}
185
186/// Returns the type name string for a simple path type, or `None`.
187fn type_name(ty: &Type) -> Option<String> {
188    if let Type::Path(p) = ty {
189        if let Some(seg) = p.path.segments.last() {
190            return Some(seg.ident.to_string());
191        }
192    }
193    None
194}
195
196/// Width rank for layout ordering: lower sorts earlier, so wider first. Types
197/// the macro cannot size (arrays, user aliases) rank widest, since placing them
198/// first cannot introduce a gap ahead of a narrower field.
199fn align_rank(ty: &Type) -> u8 {
200    // An array's alignment is its element's, so rank it that way; ranking it
201    // widest would place `[u8; 3]` ahead of a `u64` and open an internal gap.
202    if let Type::Array(a) = ty {
203        return align_rank(&a.elem);
204    }
205    match type_name(ty).as_deref() {
206        Some("u128") | Some("i128") => 0,
207        Some("u64") | Some("i64") | Some("f64") | Some("usize") | Some("isize") => 1,
208        Some("u32") | Some("f32") => 2,
209        Some("u16") => 3,
210        Some("u8") => 4,
211        _ => 0,
212    }
213}
214
215/// Classify a field's type into a [`FieldKind`].
216fn classify(ty: &Type) -> FieldKind {
217    match ty {
218        // Arrays `[T; N]` — passthrough (must be Pod).
219        Type::Array(_) => FieldKind::Passthrough,
220
221        Type::Path(p) => {
222            let seg = match p.path.segments.last() {
223                Some(s) => s,
224                None => return FieldKind::Unsupported,
225            };
226            let id = seg.ident.to_string();
227
228            match id.as_str() {
229                // Numerics — passthrough
230                "u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128"
231                | "f32" | "f64" => FieldKind::Passthrough,
232
233                "bool" => FieldKind::Bool,
234                "usize" => FieldKind::Usize,
235                "isize" => FieldKind::Isize,
236
237                "Option" => {
238                    // Extract inner type from Option<T>
239                    if let PathArguments::AngleBracketed(args) = &seg.arguments {
240                        if let Some(GenericArgument::Type(inner)) = args.args.first() {
241                            let name = type_name(inner).unwrap_or_default();
242                            let opt =
243                                |wire_ty, to_value, from_value, is_usize_isize| FieldKind::Option {
244                                    wire_ty,
245                                    to_value,
246                                    from_value,
247                                    is_usize_isize,
248                                };
249                            return match name.as_str() {
250                                "bool" => opt(
251                                    quote!(u8),
252                                    quote!(if v { 1 } else { 0 }),
253                                    quote!(raw != 0),
254                                    false,
255                                ),
256                                "f32" => opt(
257                                    quote!(u32),
258                                    quote!(v.to_bits()),
259                                    quote!(f32::from_bits(raw)),
260                                    false,
261                                ),
262                                "f64" => opt(
263                                    quote!(u64),
264                                    quote!(v.to_bits()),
265                                    quote!(f64::from_bits(raw)),
266                                    false,
267                                ),
268                                "u128" => opt(quote!(u128), quote!(v), quote!(raw), false),
269                                "i128" => {
270                                    opt(quote!(u128), quote!(v as u128), quote!(raw as i128), false)
271                                }
272                                "usize" => {
273                                    opt(quote!(u64), quote!(v as u64), quote!(raw as usize), true)
274                                }
275                                "isize" => {
276                                    opt(quote!(i64), quote!(v as i64), quote!(raw as isize), true)
277                                }
278                                "u8" | "u16" | "u32" | "u64" => {
279                                    opt(quote!(u64), quote!(v as u64), quote!(raw as #inner), false)
280                                }
281                                "i8" | "i16" | "i32" | "i64" => {
282                                    opt(quote!(i64), quote!(v as i64), quote!(raw as #inner), false)
283                                }
284                                _ => FieldKind::UnsupportedOption(name),
285                            };
286                        }
287                    }
288                    FieldKind::UnsupportedOption(String::new())
289                }
290
291                // Anything else — unrecognized, require explicit attribute
292                _ => FieldKind::Unsupported,
293            }
294        }
295
296        _ => FieldKind::Unsupported,
297    }
298}
299
300/// Derive a Pod-compatible wire struct with `From` conversions.
301///
302/// Given a struct with fields that may include `bool`, `Option<numeric>`,
303/// `usize`/`isize`, and `#[repr(u8)]` enums, generates:
304///
305/// 1. **`{Name}Wire`** — a `#[repr(C)] Clone + Copy` struct with all fields
306///    converted to Pod-safe types, plus `unsafe impl Pod`.
307/// 2. **`From<Name> for {Name}Wire`** — converts the domain struct to wire.
308/// 3. **`{Name}Wire::into_domain(self) -> Name`** — converts the wire struct
309///    back. This is an `unsafe` method for structs containing enum fields
310///    (since the enum discriminant is not validated), or a safe `From` impl
311///    for structs without enum fields.
312///
313/// # Field type mappings
314///
315/// | Source type | Wire type | To wire | From wire |
316/// |---|---|---|---|
317/// | `f32`, `f64`, `u8`..`u128`, `i8`..`i128` | same | passthrough | passthrough |
318/// | `usize` | `u64` | `as u64` | `as usize` |
319/// | `isize` | `i64` | `as i64` | `as isize` |
320/// | `bool` | `u8` | `if v { 1 } else { 0 }` | `v != 0` |
321/// | `Option<T>` (T: unsigned ≤64-bit) | `X_value: u64, X_has: u8` | `Some(v) => (v as u64, 1), None => (0, 0)` | `has != 0 => Some(value as T), else None` |
322/// | `Option<T>` (T: signed ≤64-bit) | `X_value: i64, X_has: u8` | `Some(v) => (v as i64, 1), None => (0, 0)` | `has != 0 => Some(value as T), else None` |
323/// | `Option<u128>` | `X_value: u128, X_has: u8` | `Some(v) => (v, 1), None => (0, 0)` | `has != 0 => Some(value), else None` |
324/// | `Option<i128>` | `X_value: u128, X_has: u8` | `Some(v) => (v as u128, 1), None => (0, 0)` | `has != 0 => Some(value as i128), else None` |
325/// | `Option<usize>` | `X_value: u64, X_has: u8` | `Some(v) => (v as u64, 1), None => (0, 0)` | `has != 0 => Some(value as usize), else None` |
326/// | `Option<isize>` | `X_value: i64, X_has: u8` | `Some(v) => (v as i64, 1), None => (0, 0)` | `has != 0 => Some(value as isize), else None` |
327/// | `Option<f32>` | `X_value: u32, X_has: u8` | `Some(v) => (v.to_bits(), 1), None => (0, 0)` | `has != 0 => Some(f32::from_bits(value)), else None` |
328/// | `Option<f64>` | `X_value: u64, X_has: u8` | `Some(v) => (v.to_bits(), 1), None => (0, 0)` | `has != 0 => Some(f64::from_bits(value)), else None` |
329/// | `[T; N]` (T: Pod) | same | passthrough | passthrough |
330/// | `#[photon(as_enum)] field: E` | `u8` | `v as u8` | `transmute(v)` (unsafe) |
331///
332/// # Enum fields
333///
334/// Enum fields **must** be annotated with `#[photon(as_enum)]` to opt in
335/// to the `u8` wire encoding. Without this attribute, unrecognized types
336/// produce a compile error. The enum must have `#[repr(u8)]` — the macro
337/// emits a compile-time `size_of` check to enforce this.
338///
339/// Enum fields are stored as raw `u8` on the wire. Converting back requires
340/// that the byte holds a valid discriminant. Because the macro cannot verify
341/// enum variants at compile time, structs with enum fields generate an
342/// `unsafe fn into_domain(self) -> DomainType` method on the wire struct
343/// instead of a safe `From` impl. Callers must ensure enum fields contain
344/// valid discriminants (which is always the case when the wire data was
345/// produced by a valid domain value via `From<Domain> for Wire`).
346///
347/// # Example
348///
349/// ```ignore
350/// #[repr(u8)]
351/// #[derive(Clone, Copy)]
352/// enum Side { Buy = 0, Sell = 1 }
353///
354/// #[derive(photon_ring::Message)]
355/// struct Order {
356///     price: f64,
357///     qty: u32,
358///     #[photon(as_enum)]
359///     side: Side,
360///     filled: bool,
361///     tag: Option<u32>,
362/// }
363/// // Generates: OrderWire, From<Order> for OrderWire,
364/// //            OrderWire::into_domain (unsafe, due to enum field)
365/// ```
366#[proc_macro_derive(Message, attributes(photon))]
367pub fn derive_message(input: TokenStream) -> TokenStream {
368    let input = parse_macro_input!(input as DeriveInput);
369    let name = &input.ident;
370    let wire_name = format_ident!("{}Wire", name);
371
372    // Only named structs are supported
373    let fields = match &input.data {
374        Data::Struct(s) => match &s.fields {
375            Fields::Named(f) => f.named.iter().collect::<Vec<_>>(),
376            _ => {
377                return syn::Error::new_spanned(
378                    &input.ident,
379                    "Message can only be derived for structs with named fields",
380                )
381                .to_compile_error()
382                .into();
383            }
384        },
385        _ => {
386            return syn::Error::new_spanned(
387                &input.ident,
388                "Message can only be derived for structs",
389            )
390            .to_compile_error()
391            .into();
392        }
393    };
394
395    let mut wire_fields = Vec::new();
396    let mut wire_types: Vec<proc_macro2::TokenStream> = Vec::new();
397    // Rank by width so fields can be emitted widest-first: no internal padding.
398    let mut wire_ranks: Vec<u8> = Vec::new();
399    let mut to_wire = Vec::new();
400    let mut from_wire = Vec::new();
401    let mut assertions = Vec::new();
402    let mut has_enum_fields = false;
403    let mut has_usize_isize = false;
404
405    for field in &fields {
406        let fname = field.ident.as_ref().unwrap();
407        let fty = &field.ty;
408
409        // Check for #[photon(as_enum)] attribute
410        let is_explicit_enum = field.attrs.iter().any(|attr| {
411            if attr.path().is_ident("photon") {
412                if let Ok(meta) = attr.parse_args::<syn::Ident>() {
413                    return meta == "as_enum";
414                }
415            }
416            false
417        });
418
419        let kind = if is_explicit_enum {
420            FieldKind::Enum
421        } else {
422            classify(fty)
423        };
424
425        match kind {
426            FieldKind::Passthrough => {
427                // A passthrough field lands in the wire struct unchanged, and the
428                // wire struct gets `unsafe impl Pod`. Prove the field really is
429                // Pod: `[bool; 2]` and an alias that merely looks like a
430                // primitive would otherwise ride through on syntax alone.
431                assertions.push(quote! {
432                    const _: () = {
433                        fn _assert_pod<T: photon_ring::Pod>() {}
434                        fn _check() { _assert_pod::<#fty>(); }
435                    };
436                });
437                wire_fields.push(quote! { pub #fname: #fty });
438                wire_types.push(quote!(#fty));
439                wire_ranks.push(align_rank(fty));
440                to_wire.push(quote! { #fname: src.#fname });
441                from_wire.push(quote! { #fname: src.#fname });
442            }
443            FieldKind::Bool => {
444                wire_fields.push(quote! { pub #fname: u8 });
445                wire_types.push(quote!(u8));
446                wire_ranks.push(4);
447                to_wire.push(quote! { #fname: if src.#fname { 1 } else { 0 } });
448                from_wire.push(quote! { #fname: src.#fname != 0 });
449            }
450            FieldKind::Usize => {
451                has_usize_isize = true;
452                wire_fields.push(quote! { pub #fname: u64 });
453                wire_types.push(quote!(u64));
454                wire_ranks.push(1);
455                to_wire.push(quote! { #fname: src.#fname as u64 });
456                from_wire.push(quote! { #fname: src.#fname as usize });
457            }
458            FieldKind::Isize => {
459                has_usize_isize = true;
460                wire_fields.push(quote! { pub #fname: i64 });
461                wire_types.push(quote!(i64));
462                wire_ranks.push(1);
463                to_wire.push(quote! { #fname: src.#fname as i64 });
464                from_wire.push(quote! { #fname: src.#fname as isize });
465            }
466            FieldKind::Option {
467                wire_ty,
468                to_value,
469                from_value,
470                is_usize_isize,
471            } => {
472                if is_usize_isize {
473                    has_usize_isize = true;
474                }
475                let value_field = format_ident!("{}_value", fname);
476                let has_field = format_ident!("{}_has", fname);
477                wire_fields.push(quote! { pub #value_field: #wire_ty });
478                wire_types.push(quote!(#wire_ty));
479                wire_ranks.push(match wire_ty.to_string().as_str() {
480                    "u128" => 0,
481                    "u32" => 2,
482                    "u16" => 3,
483                    "u8" => 4,
484                    _ => 1,
485                });
486                wire_fields.push(quote! { pub #has_field: u8 });
487                wire_types.push(quote!(u8));
488                wire_ranks.push(4);
489                to_wire.push(quote! {
490                    #value_field: match src.#fname {
491                        Some(v) => #to_value,
492                        None => 0,
493                    }
494                });
495                to_wire.push(quote! {
496                    #has_field: if src.#fname.is_some() { 1 } else { 0 }
497                });
498                from_wire.push(quote! {
499                    #fname: if src.#has_field != 0 {
500                        let raw = src.#value_field;
501                        Some(#from_value)
502                    } else {
503                        None
504                    }
505                });
506            }
507            FieldKind::Enum => {
508                has_enum_fields = true;
509                wire_fields.push(quote! { pub #fname: u8 });
510                wire_types.push(quote!(u8));
511                wire_ranks.push(4);
512                to_wire.push(quote! { #fname: src.#fname as u8 });
513                from_wire.push(quote! {
514                    // SAFETY: This transmute converts a raw u8 back to the enum type.
515                    // This is sound ONLY when the byte contains a valid discriminant.
516                    // The wire struct should only be constructed via `From<DomainType>`,
517                    // which guarantees valid discriminants. Constructing the wire struct
518                    // from arbitrary bytes and calling `into_domain()` is undefined
519                    // behavior if any enum field holds an invalid discriminant.
520                    #fname: unsafe { core::mem::transmute::<u8, #fty>(src.#fname) }
521                });
522                // Compile-time assertion: enum must be 1 byte (#[repr(u8)])
523                let msg = format!(
524                    "Message derive: field `{}` has type `{}` which is not 1 byte. \
525                     Enum fields must have #[repr(u8)].",
526                    fname,
527                    quote! { #fty },
528                );
529                let msg_lit = syn::LitStr::new(&msg, Span::call_site());
530                assertions.push(quote! {
531                    const _: () = {
532                        assert!(
533                            core::mem::size_of::<#fty>() == 1,
534                            #msg_lit,
535                        );
536                    };
537                });
538            }
539            FieldKind::Unsupported => {
540                let msg = format!(
541                    "Unsupported field type `{}`. Use #[photon(as_enum)] for #[repr(u8)] enum fields, \
542                     or convert to a numeric type manually.",
543                    quote!(#fty),
544                );
545                return syn::Error::new_spanned(fty, msg).to_compile_error().into();
546            }
547            FieldKind::UnsupportedOption(inner_name) => {
548                let msg = format!(
549                    "Message derive: field `{}` has unsupported type `Option<{}>`. \
550                     Only Option<bool>, Option<integer>, Option<f32>, and Option<f64> \
551                     are supported.",
552                    fname, inner_name,
553                );
554                return syn::Error::new_spanned(fty, msg).to_compile_error().into();
555            }
556        }
557    }
558
559    // H5: Compile-time assertion that usize/isize fit in u64/i64 (documents
560    // the 64-bit assumption and fails loudly on platforms where it does not hold).
561    if has_usize_isize {
562        assertions.push(quote! {
563            const _: () = assert!(
564                core::mem::size_of::<usize>() <= core::mem::size_of::<u64>(),
565                "photon-ring Message derive requires usize to fit in u64",
566            );
567        });
568    }
569
570    // If the struct has enum fields, generate an unsafe `into_domain` method
571    // instead of a safe `From` impl to avoid exposing transmute through safe code.
572    let from_wire_impl = if has_enum_fields {
573        quote! {
574            impl #wire_name {
575                /// Convert wire struct back to domain struct.
576                ///
577                /// # Safety
578                ///
579                /// Enum fields are stored as raw `u8` and converted back via
580                /// `core::mem::transmute`. The caller **must** ensure every enum
581                /// field contains a valid discriminant value. This is guaranteed
582                /// when the wire struct was produced by `From<DomainType>` — but
583                /// constructing the wire struct from arbitrary bytes (e.g. reading
584                /// raw memory, deserialization) and calling this method is
585                /// **undefined behavior** if any enum field holds an invalid
586                /// discriminant.
587                #[inline]
588                pub unsafe fn into_domain(self) -> #name {
589                    let src = self;
590                    #name {
591                        #(#from_wire),*
592                    }
593                }
594            }
595        }
596    } else {
597        quote! {
598            impl From<#wire_name> for #name {
599                #[inline]
600                fn from(src: #wire_name) -> Self {
601                    #name {
602                        #(#from_wire),*
603                    }
604                }
605            }
606        }
607    };
608
609    // C3: Add a doc warning on the wire struct when it contains enum fields
610    let wire_struct_doc = if has_enum_fields {
611        quote! {
612            /// Auto-generated Pod-compatible wire struct for the domain type.
613            ///
614            /// # Warning
615            ///
616            /// This struct contains enum fields stored as raw `u8`. Constructing
617            /// it from arbitrary bytes (not via `From<DomainType>`) and then calling
618            /// `into_domain()` can cause **undefined behavior** if any enum field
619            /// holds an invalid discriminant value.
620        }
621    } else {
622        quote! {
623            /// Auto-generated Pod-compatible wire struct for the domain type.
624        }
625    };
626
627    // Emit widest-first so the C layout cannot insert gaps between fields, then
628    // pad the tail to a whole number of alignment units. Together these make the
629    // generated struct padding-free, which `Pod` requires.
630    let mut ordered: Vec<usize> = (0..wire_fields.len()).collect();
631    ordered.sort_by_key(|&i| wire_ranks[i]);
632    let wire_fields: Vec<_> = ordered.iter().map(|&i| wire_fields[i].clone()).collect();
633    let wire_types: Vec<_> = ordered.iter().map(|&i| wire_types[i].clone()).collect();
634
635    let pad_const = format_ident!("__{}_TAIL_PAD", wire_name.to_string().to_uppercase());
636
637    let expanded = quote! {
638        // Compile-time assertions
639        #(#assertions)*
640
641        #[doc(hidden)]
642        const #pad_const: usize = {
643            let sum = 0usize #( + core::mem::size_of::<#wire_types>() )*;
644            let align = { let mut a = 1usize; #( { let f = core::mem::align_of::<#wire_types>(); if f > a { a = f; } } )* a };
645            (align - sum % align) % align
646        };
647
648        #wire_struct_doc
649        #[repr(C)]
650        #[derive(Clone, Copy)]
651        pub struct #wire_name {
652            #(#wire_fields,)*
653            /// Explicit tail padding. `Pod` forbids implicit padding, because an
654            /// uninitialised gap is undefined to read as part of a value; making
655            /// it a real field means every byte is initialised.
656            pub _pad: [u8; #pad_const],
657        }
658
659        // `Pod` forbids padding, and a #[repr(C)] struct of mixed-width numerics
660        // readily acquires it. An implicit gap is uninitialised memory, and the
661        // `atomic-slots` payload copy reads the value as integer chunks, so a
662        // padded wire struct is undefined behaviour rather than merely wasteful.
663        // Size equal to the sum of the field sizes is exactly the no-padding
664        // condition. If this fails, order the source struct's fields widest
665        // first and the generated layout becomes padding-free.
666        const _: () = {
667            let sum = 0usize #( + core::mem::size_of::<#wire_types>() )* + #pad_const;
668            assert!(
669                core::mem::size_of::<#wire_name>() == sum,
670                "photon-ring: the generated wire struct has internal padding, which \
671                 is not a valid Pod. The macro orders fields by width, but cannot \
672                 see through a type alias, so one of them landed out of order. \
673                 Use a concrete primitive or array type for that field, or add an \
674                 explicit padding field to close the gap.",
675            );
676        };
677
678        // Safety: all fields of the wire struct are plain numeric types
679        // (u8, u32, u64, f32, f64, etc.) where every bit pattern is valid, and
680        // the assertion above establishes there is no padding between them.
681        unsafe impl photon_ring::Pod for #wire_name {}
682
683        impl From<#name> for #wire_name {
684            #[inline]
685            fn from(src: #name) -> Self {
686                #wire_name {
687                    #(#to_wire,)*
688                    _pad: [0; #pad_const],
689                }
690            }
691        }
692
693        #from_wire_impl
694    };
695
696    TokenStream::from(expanded)
697}