msgpack_tagged_derive/
lib.rs

1//! Companion proc-macro crate for `msgpack_tagged`.
2//!
3//! Handles named-field structs, tuple structs / newtypes, and enums end-to-end:
4//! parses `#[tag(N)]` annotations, builds a `Tagged::Product` (struct-shaped)
5//! or `Tagged::Sum` (enum-shaped) wire description, and emits a `register_into`
6//! that registers `Self` and recurses into each field/variant payload type.
7//! Unit structs and unions still fall through to a stub expansion until
8//! subsequent steps add their handling.
9//!
10//! Per-variant struct/tuple field tagging on enum variants is the next
11//! incremental step — at this point every enum variant gets an *empty*
12//! payload `Product`, and any `#[tag(...)]` on a variant's field is rejected.
13//!
14//! Design: [issue #12554](https://github.com/noir-lang/noir/issues/12554).
15
16use proc_macro::TokenStream;
17use proc_macro2::TokenStream as TokenStream2;
18use quote::{ToTokens, quote};
19use syn::{
20    Attribute, Data, DataEnum, DataStruct, DeriveInput, Expr, ExprLit, Field, Fields, GenericParam,
21    Ident, Lit, LitInt, Meta, Token, Type, Variant, WhereClause, WherePredicate,
22    parse::{Parse, ParseStream},
23    parse_macro_input, parse_quote,
24    punctuated::Punctuated,
25};
26
27#[proc_macro_derive(MsgpackTagged, attributes(tag, tagged))]
28pub fn derive_msgpack_tagged(input: TokenStream) -> TokenStream {
29    let input = parse_macro_input!(input as DeriveInput);
30    expand(&input).unwrap_or_else(syn::Error::into_compile_error).into()
31}
32
33/// Build a bare `Product { ... }` struct literal from parsed field entries
34/// plus the reserved list and unknown-tag policy. Used both for top-level
35/// struct shapes (wrapped in `Tagged::Product(...)`) and for the inner
36/// payload of enum variants (used unwrapped).
37fn product_struct_literal(
38    entries: &[TaggedField<'_>],
39    reserved: &[u8],
40    allow_unknown_tags: bool,
41    tag_order_matches_source: bool,
42) -> TokenStream2 {
43    let field_entries = entries.iter().map(|e| {
44        let tag = e.tag;
45        let name = &e.name;
46        quote! { (#tag, #name) }
47    });
48    let reserved_entries = reserved.iter().map(|tag| quote! { #tag });
49    quote! {
50        ::msgpack_tagged::Product {
51            fields: &[#(#field_entries),*],
52            reserved: &[#(#reserved_entries),*],
53            allow_unknown_tags: #allow_unknown_tags,
54            tag_order_matches_source: #tag_order_matches_source,
55        }
56    }
57}
58
59/// Build a `Tagged::Product(Product { ... })` literal — top-level
60/// struct/tuple-struct emission. Wraps [`product_struct_literal`].
61fn product_literal(
62    entries: &[TaggedField<'_>],
63    reserved: &[u8],
64    allow_unknown_tags: bool,
65    tag_order_matches_source: bool,
66) -> TokenStream2 {
67    let inner =
68        product_struct_literal(entries, reserved, allow_unknown_tags, tag_order_matches_source);
69    quote! { ::msgpack_tagged::Tagged::Product(#inner) }
70}
71
72/// Render a `VariantKind` discriminator as the matching `::msgpack_tagged`
73/// path expression — used inside generated `Variant` literals.
74fn variant_kind_token(kind: VariantKind) -> TokenStream2 {
75    match kind {
76        VariantKind::Unit => quote! { ::msgpack_tagged::VariantKind::Unit },
77        VariantKind::Newtype => quote! { ::msgpack_tagged::VariantKind::Newtype },
78        VariantKind::Tuple => quote! { ::msgpack_tagged::VariantKind::Tuple },
79        VariantKind::Struct => quote! { ::msgpack_tagged::VariantKind::Struct },
80    }
81}
82
83/// Reject the variant-level payload-shape modifiers (`reserved(...)` and
84/// `allow_unknown_tags`) on a variant that has no payload field tag space —
85/// unit and newtype variants — since neither flag has anything to govern
86/// there. Surfaces the mistake at derive time rather than silently dropping
87/// the flag.
88fn reject_payload_only_attrs_on_empty_variant(
89    variant: &Variant,
90    variant_attrs: &VariantAttrs,
91) -> syn::Result<()> {
92    if !variant_attrs.reserved.is_empty() {
93        return Err(syn::Error::new_spanned(
94            variant,
95            "`#[tagged(reserved(...))]` on a unit or newtype variant has no effect — \
96             the payload has no field tag space to reserve into",
97        ));
98    }
99    if variant_attrs.allow_unknown_tags {
100        return Err(syn::Error::new_spanned(
101            variant,
102            "`#[tagged(allow_unknown_tags)]` on a unit or newtype variant has no effect — \
103             the payload has no field tag space to skip unknown tags from",
104        ));
105    }
106    Ok(())
107}
108
109/// Empty `Tagged::Product` literal — used by newtypes, `via`-delegating
110/// types, the stub expansion, and any other shape that contributes no wire
111/// metadata of its own.
112fn empty_product_literal() -> TokenStream2 {
113    quote! {
114        ::msgpack_tagged::Tagged::empty_product()
115    }
116}
117
118/// Build a `Tagged::Sum` literal from variant entries, the enum-level
119/// reserved variant-tag list, and the runtime decode-policy flags. Each
120/// variant's `payload` is rendered as a `Product` populated from the
121/// variant's parsed tagged fields, plus its variant-level
122/// `#[tagged(reserved(...))]` and `#[tagged(allow_unknown_tags)]` flags.
123fn sum_literal(
124    variants: &[TaggedVariant<'_>],
125    reserved: &[u8],
126    on_reserved_tag: Option<u8>,
127    on_unknown_tag: Option<u8>,
128) -> TokenStream2 {
129    let variant_entries = variants.iter().map(|v| {
130        let tag = v.tag;
131        let name = &v.name;
132        let kind = variant_kind_token(v.kind);
133        let payload = product_struct_literal(
134            &v.payload,
135            &v.payload_reserved,
136            v.payload_allow_unknown_tags,
137            v.payload_tag_order_matches_source,
138        );
139        quote! {
140            ::msgpack_tagged::Variant {
141                tag: #tag,
142                name: #name,
143                kind: #kind,
144                payload: #payload,
145            }
146        }
147    });
148    let reserved_entries = reserved.iter().map(|tag| quote! { #tag });
149    let option_u8 = |o: Option<u8>| match o {
150        Some(t) => quote! { ::core::option::Option::Some(#t) },
151        None => quote! { ::core::option::Option::None },
152    };
153    let on_reserved_tag = option_u8(on_reserved_tag);
154    let on_unknown_tag = option_u8(on_unknown_tag);
155    quote! {
156        ::msgpack_tagged::Tagged::Sum(::msgpack_tagged::Sum {
157            variants: &[#(#variant_entries),*],
158            reserved: &[#(#reserved_entries),*],
159            on_reserved_tag: #on_reserved_tag,
160            on_unknown_tag: #on_unknown_tag,
161        })
162    }
163}
164
165fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
166    let type_attrs = parse_tagged_type_attrs(input)?;
167
168    // `via(...)` short-circuits the rest of expansion regardless of shape:
169    // struct, tuple struct, or enum — they all delegate to the wire DTO. The
170    // public type's own fields/variants are wire-irrelevant in this case, so
171    // we also reject any field-level `#[tag(...)]` annotations that would
172    // suggest otherwise.
173    if let Some(wire_type) = &type_attrs.via {
174        validate_no_field_tag_attrs(input)?;
175        return Ok(expand_via(input, wire_type));
176    }
177
178    match &input.data {
179        Data::Struct(DataStruct { fields: Fields::Named(named), .. }) => {
180            expand_named_struct(input, &named.named, &type_attrs)
181        }
182        Data::Struct(DataStruct { fields: Fields::Unnamed(unnamed), .. }) => {
183            expand_unnamed_struct(input, &unnamed.unnamed, &type_attrs)
184        }
185        Data::Enum(data) => expand_enum(input, data, &type_attrs),
186        // Unit structs and unions: stub for now. Real expansion lands in
187        // subsequent steps.
188        _ => Ok(stub(input)),
189    }
190}
191
192/// Dispatch for tuple structs (`struct Foo(A, B)`). Single-field tuple
193/// structs are *newtypes* and pass through to the inner type without a
194/// registry entry of their own; multi-field tuple structs register
195/// themselves with positional names ("0", "1", …).
196fn expand_unnamed_struct(
197    input: &DeriveInput,
198    fields: &Punctuated<Field, Token![,]>,
199    type_attrs: &TypeAttrs,
200) -> syn::Result<TokenStream2> {
201    debug_assert!(type_attrs.via.is_none()); // handled in `expand`
202    if fields.len() == 1 {
203        expand_newtype(input, fields.first().unwrap(), type_attrs)
204    } else {
205        expand_tuple_struct(input, fields, type_attrs)
206    }
207}
208
209/// Newtype (single-element tuple struct): wire bytes are exactly the inner
210/// type's bytes. The newtype itself doesn't get a registry entry — only its
211/// inner type does (via the recursive `register_into`). Type-level
212/// `reserved`/`allow_unknown_tags` are inert and rejected for clarity.
213fn expand_newtype(
214    input: &DeriveInput,
215    inner_field: &Field,
216    type_attrs: &TypeAttrs,
217) -> syn::Result<TokenStream2> {
218    if !type_attrs.reserved.is_empty() {
219        return Err(syn::Error::new_spanned(
220            input,
221            "newtype structs (single-element tuple structs) pass through to the inner type \
222             and have no wire shape of their own — `#[tagged(reserved(...))]` doesn't apply",
223        ));
224    }
225    if type_attrs.allow_unknown_tags {
226        return Err(syn::Error::new_spanned(
227            input,
228            "newtype structs (single-element tuple structs) pass through to the inner type \
229             and have no wire shape of their own — `#[tagged(allow_unknown_tags)]` doesn't apply",
230        ));
231    }
232    for attr in &inner_field.attrs {
233        if attr.path().is_ident("tag") {
234            return Err(syn::Error::new_spanned(
235                attr,
236                "newtype structs pass through to the inner type — \
237                 `#[tag(...)]` on the inner field is not allowed",
238            ));
239        }
240    }
241
242    let name = &input.ident;
243    let inner_type = &inner_field.ty;
244    let where_clause = build_passthrough_where_clause(input, inner_type);
245    let (impl_generics, ty_generics, _) = input.generics.split_for_impl();
246    let tagged = empty_product_literal();
247
248    Ok(quote! {
249        impl #impl_generics ::msgpack_tagged::MsgpackTagged for #name #ty_generics #where_clause {
250            const TAGGED: ::msgpack_tagged::Tagged = #tagged;
251
252            fn register_into(_reg: &mut ::msgpack_tagged::TagRegistry) {
253                <#inner_type as ::msgpack_tagged::MsgpackTagged>::register_into(_reg);
254            }
255        }
256    })
257}
258
259/// Multi-element tuple struct (`struct Pair(A, B, ...)`). Tagging style must
260/// be uniform: either every field carries `#[tag(N)]` (explicit, allows
261/// reordering / `default`) or none do (implicit positional 0, 1, 2, …).
262/// Mixing is rejected.
263///
264/// To be clear, even with positional tagging, the tags becomes keys in a map,
265/// not indexes in an array, they just don't have to be spelled out. As such,
266/// they can be reserved, if one field replaces another in a newer version.
267///
268/// Field names in `TAGS` are positional strings ("0", "1", …) — the wrapper
269/// Serializer addresses tuple-struct fields positionally, not by name, so
270/// the names are placeholders.
271fn expand_tuple_struct(
272    input: &DeriveInput,
273    fields: &Punctuated<Field, Token![,]>,
274    type_attrs: &TypeAttrs,
275) -> syn::Result<TokenStream2> {
276    let name = &input.ident;
277    let name_str = parse_serde_rename(input)?.unwrap_or_else(|| name.to_string());
278    let reserved = &type_attrs.reserved;
279    let allow_unknown_tags = type_attrs.allow_unknown_tags;
280
281    let (entries, tag_order_matches_source) = parse_tuple_fields(input, fields, reserved)?;
282
283    let recursion_calls = entries.iter().map(|e| {
284        let ty = e.ty;
285        quote! { <#ty as ::msgpack_tagged::MsgpackTagged>::register_into(_reg); }
286    });
287
288    let tagged = product_literal(&entries, reserved, allow_unknown_tags, tag_order_matches_source);
289    let where_clause = build_where_clause(input, &entries, &type_attrs.extra_bounds);
290    let (impl_generics, ty_generics, _) = input.generics.split_for_impl();
291
292    Ok(quote! {
293        impl #impl_generics ::msgpack_tagged::MsgpackTagged for #name #ty_generics #where_clause {
294            const TAGGED: ::msgpack_tagged::Tagged = #tagged;
295
296            fn register_into(_reg: &mut ::msgpack_tagged::TagRegistry) {
297                if _reg.try_insert::<Self>(#name_str) {
298                    #(#recursion_calls)*
299                }
300            }
301        }
302    })
303}
304
305/// Variant shape on the wire. Mirrors `msgpack_tagged::VariantKind` and
306/// drives both the kind discriminator the macro emits and the payload-shape
307/// dispatch above.
308#[derive(Clone, Copy)]
309enum VariantKind {
310    Unit,
311    Newtype,
312    Tuple,
313    Struct,
314}
315
316/// Per-tagged-variant info collected during enum macro expansion. `name` is
317/// the variant's wire-name (its Rust ident, as a string). `payload` holds
318/// the parsed payload-field entries — empty for unit and newtype variants,
319/// populated by [`parse_named_fields`] for struct-shaped variants and
320/// [`parse_tuple_fields`] for tuple-shaped variants (with two-or-more
321/// fields). The entries drive both the variant's emitted payload `Product`
322/// and the per-field bounds (`MsgpackTagged`, `Default`) in the impl's where
323/// clause.
324///
325/// `kind` is the [`VariantKind`] discriminator. For [`VariantKind::Newtype`],
326/// `newtype_inner` carries the inner field's type — its `MsgpackTagged` bound
327/// and `register_into` recursion are emitted separately from the (empty)
328/// payload.
329///
330/// `payload_reserved` and `payload_allow_unknown_tags` are the variant-level
331/// `#[tagged(reserved(...))]` and `#[tagged(allow_unknown_tags)]` flags,
332/// scoped to the variant's *payload field* tag space (not to the variant
333/// tag itself — that's governed by the enclosing type's `#[tagged(...)]`).
334struct TaggedVariant<'a> {
335    tag: u8,
336    name: String,
337    kind: VariantKind,
338    payload: Vec<TaggedField<'a>>,
339    newtype_inner: Option<&'a Type>,
340    payload_reserved: Vec<u8>,
341    payload_allow_unknown_tags: bool,
342    /// Whether the variant payload's source-declaration order is already
343    /// tag-ascending. Computed pre-sort in `parse_named_fields` /
344    /// `parse_tuple_fields` and propagated into the emitted payload
345    /// `Product` so the encoder can skip the buffer-and-sort flush under
346    /// the `Array` strategy.
347    payload_tag_order_matches_source: bool,
348}
349
350/// Enum (`enum E { A, B(...), C { ... } }`). Each variant carries an
351/// explicit `#[tag(N)]`; the variant tag is what goes on the wire as the
352/// discriminator. The expansion emits a `Tagged::Sum` listing every variant
353/// in tag-ascending order, and a `register_into` that registers `Self` and
354/// recurses into every tagged variant-payload field type so nested
355/// `MsgpackTagged` types are reached.
356///
357/// Variant payloads carry their own `#[tag(N)]` annotations: named-variant
358/// fields use the same "every field needs an explicit tag (or auto-skip)"
359/// rule as top-level named structs, and tuple-variant fields use the same
360/// all-or-nothing implicit/explicit positional rule as top-level tuple
361/// structs. `#[tagged(reserved(...))]` at the enum level applies only to
362/// the variant tags, not the field tags inside any variant's payload —
363/// each variant's payload starts with an empty reserved list.
364///
365/// `#[tagged(allow_unknown_tags)]` is rejected on enums: an unknown variant
366/// tag has no skip semantics — there's no fragment to skip, since the
367/// value's discriminator itself is unknown — so the flag would have nowhere
368/// to land in the wire shape. Use a `#[tagged(on_unknown)]` unit variant
369/// instead — the wrapper routes unknown wire tags there on decode.
370fn expand_enum(
371    input: &DeriveInput,
372    data: &DataEnum,
373    type_attrs: &TypeAttrs,
374) -> syn::Result<TokenStream2> {
375    debug_assert!(type_attrs.via.is_none()); // handled in `expand`
376    if type_attrs.allow_unknown_tags {
377        return Err(syn::Error::new_spanned(
378            input,
379            "`#[tagged(allow_unknown_tags)]` doesn't apply to enums — there's no \
380             meaningful skip semantics for an unknown variant tag (the value's \
381             discriminator itself becomes non-representable). Mark a unit variant \
382             with `#[tagged(on_unknown)]` instead — the wrapper will route \
383             unknown wire tags there on decode",
384        ));
385    }
386    let name = &input.ident;
387    let name_str = parse_serde_rename(input)?.unwrap_or_else(|| name.to_string());
388    let reserved = &type_attrs.reserved;
389
390    let mut variants: Vec<TaggedVariant<'_>> = Vec::with_capacity(data.variants.len());
391    let mut seen_tags = std::collections::HashSet::new();
392    let mut on_reserved_marker: Option<(u8, String)> = None;
393    let mut on_unknown_marker: Option<(u8, String)> = None;
394    for variant in &data.variants {
395        let tag = parse_variant_tag(variant, reserved)?;
396        if !seen_tags.insert(tag) {
397            return Err(syn::Error::new_spanned(
398                variant,
399                format!("variant tag {tag} is used more than once"),
400            ));
401        }
402        // Variant-level `#[tagged(...)]` covers two concerns: payload-shape
403        // (`reserved(...)`, `allow_unknown_tags`) and fallback-routing
404        // markers (`on_reserved`, `on_unknown`). The latter must be on unit
405        // variants — the wrapper discards the payload bytes before visiting
406        // the fallback, so the variant can't carry one of its own.
407        let variant_attrs = parse_tagged_variant_attrs(variant)?;
408        if (variant_attrs.on_reserved || variant_attrs.on_unknown)
409            && !matches!(variant.fields, Fields::Unit)
410        {
411            return Err(syn::Error::new_spanned(
412                variant,
413                "`#[tagged(on_reserved)]` and `#[tagged(on_unknown)]` mark fallback \
414                 routing targets — the wrapper discards the wire payload when it \
415                 routes here, so they're only valid on unit variants",
416            ));
417        }
418        if variant_attrs.on_reserved {
419            if let Some((_, prev)) = &on_reserved_marker {
420                return Err(syn::Error::new_spanned(
421                    variant,
422                    format!(
423                        "multiple `#[tagged(on_reserved)]` variants on the same enum — \
424                         only one fallback for retired tags is allowed (previous: {prev:?})",
425                    ),
426                ));
427            }
428            on_reserved_marker = Some((tag, variant.ident.to_string()));
429        }
430        if variant_attrs.on_unknown {
431            if let Some((_, prev)) = &on_unknown_marker {
432                return Err(syn::Error::new_spanned(
433                    variant,
434                    format!(
435                        "multiple `#[tagged(on_unknown)]` variants on the same enum — \
436                         only one fallback for unknown tags is allowed (previous: {prev:?})",
437                    ),
438                ));
439            }
440            on_unknown_marker = Some((tag, variant.ident.to_string()));
441        }
442        let (kind, payload, payload_tag_order_matches_source, newtype_inner) = match &variant.fields
443        {
444            Fields::Unit => {
445                reject_payload_only_attrs_on_empty_variant(variant, &variant_attrs)?;
446                // No payload ⇒ trivially monotonic.
447                (VariantKind::Unit, Vec::new(), true, None)
448            }
449            Fields::Named(named) => {
450                let (payload, monotonic) =
451                    parse_named_fields(&named.named, &variant_attrs.reserved)?;
452                (VariantKind::Struct, payload, monotonic, None)
453            }
454            Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
455                // Single-element tuple variant is a *newtype variant*: its wire
456                // bytes are exactly the inner type's bytes under the variant
457                // tag — there is no field-level tag map. Reject `#[tag(...)]`
458                // on the inner field (it would imply field-level tagging that
459                // the wire shape can't express) and reject the variant-level
460                // payload-shape attrs that have nothing to govern (no field
461                // tag space exists).
462                let inner = unnamed.unnamed.first().expect("len == 1");
463                for attr in &inner.attrs {
464                    if attr.path().is_ident("tag") {
465                        return Err(syn::Error::new_spanned(
466                            attr,
467                            "newtype variants (single-element tuple variants) pass through to \
468                             the inner type — `#[tag(...)]` on the inner field is not allowed",
469                        ));
470                    }
471                }
472                reject_payload_only_attrs_on_empty_variant(variant, &variant_attrs)?;
473                (VariantKind::Newtype, Vec::new(), true, Some(&inner.ty))
474            }
475            Fields::Unnamed(unnamed) => {
476                let (payload, monotonic) =
477                    parse_tuple_fields(variant, &unnamed.unnamed, &variant_attrs.reserved)?;
478                (VariantKind::Tuple, payload, monotonic, None)
479            }
480        };
481        variants.push(TaggedVariant {
482            tag,
483            name: variant.ident.to_string(),
484            kind,
485            payload,
486            newtype_inner,
487            payload_reserved: variant_attrs.reserved,
488            payload_allow_unknown_tags: variant_attrs.allow_unknown_tags,
489            payload_tag_order_matches_source,
490        });
491    }
492    variants.sort_by_key(|v| v.tag);
493
494    let recursion_calls = variants.iter().flat_map(|v| {
495        // Payload fields (Struct + Tuple variants) and the newtype-variant
496        // inner type both need to be reached so any nested `MsgpackTagged`
497        // types end up in the registry.
498        let payload_calls = v.payload.iter().map(|entry| {
499            let ty = entry.ty;
500            quote! { <#ty as ::msgpack_tagged::MsgpackTagged>::register_into(_reg); }
501        });
502        let newtype_call = v.newtype_inner.map(|ty| {
503            quote! { <#ty as ::msgpack_tagged::MsgpackTagged>::register_into(_reg); }
504        });
505        payload_calls.chain(newtype_call)
506    });
507
508    let on_reserved_tag = on_reserved_marker.map(|(tag, _)| tag);
509    let on_unknown_tag = on_unknown_marker.map(|(tag, _)| tag);
510    let tagged = sum_literal(&variants, reserved, on_reserved_tag, on_unknown_tag);
511    let where_clause = build_enum_where_clause(input, &variants, &type_attrs.extra_bounds);
512    let (impl_generics, ty_generics, _) = input.generics.split_for_impl();
513
514    Ok(quote! {
515        impl #impl_generics ::msgpack_tagged::MsgpackTagged for #name #ty_generics #where_clause {
516            const TAGGED: ::msgpack_tagged::Tagged = #tagged;
517
518            fn register_into(_reg: &mut ::msgpack_tagged::TagRegistry) {
519                if _reg.try_insert::<Self>(#name_str) {
520                    #(#recursion_calls)*
521                }
522            }
523        }
524    })
525}
526
527/// Parse the (required) `#[tag(N)]` attribute on an enum variant. Rejects the
528/// `skip` form and the `default` modifier — neither has clear semantics for a
529/// variant — and rejects tags that collide with the type's reserved list.
530fn parse_variant_tag(variant: &Variant, reserved: &[u8]) -> syn::Result<u8> {
531    let mut found: Option<(&Attribute, TagArgs)> = None;
532    for attr in &variant.attrs {
533        if !attr.path().is_ident("tag") {
534            continue;
535        }
536        if found.is_some() {
537            return Err(syn::Error::new_spanned(attr, "duplicate `#[tag(...)]` attribute"));
538        }
539        found = Some((attr, attr.parse_args()?));
540    }
541    let Some((attr, args)) = found else {
542        return Err(syn::Error::new_spanned(
543            variant,
544            "missing `#[tag(N)]` attribute on enum variant — every variant needs an explicit tag",
545        ));
546    };
547    let TagArgs(tag) = args;
548    if reserved.contains(&tag) {
549        return Err(syn::Error::new_spanned(
550            attr,
551            format!(
552                "tag {tag} is in the type's `#[tagged(reserved(...))]` list — pick a different tag, or remove it from the reserved list"
553            ),
554        ));
555    }
556    Ok(tag)
557}
558
559/// Where clause for an enum impl. Same shape as `build_where_clause` for
560/// structs — `T: 'static` per type parameter, plus a deduped
561/// `<PayloadFieldType>: MsgpackTagged` bound for every tagged field type
562/// appearing in any variant's payload.
563fn build_enum_where_clause(
564    input: &DeriveInput,
565    variants: &[TaggedVariant<'_>],
566    extra_bounds: &[WherePredicate],
567) -> Option<WhereClause> {
568    let has_type_params = input.generics.params.iter().any(|p| matches!(p, GenericParam::Type(_)));
569    let any_bound_source =
570        variants.iter().any(|v| !v.payload.is_empty() || v.newtype_inner.is_some());
571
572    if !any_bound_source && !has_type_params && extra_bounds.is_empty() {
573        return input.generics.where_clause.clone();
574    }
575
576    let mut where_clause = input.generics.where_clause.clone().unwrap_or_else(|| WhereClause {
577        where_token: <Token![where]>::default(),
578        predicates: Punctuated::new(),
579    });
580
581    for param in &input.generics.params {
582        if let GenericParam::Type(type_param) = param {
583            let ident = &type_param.ident;
584            where_clause.predicates.push(parse_quote!(#ident: 'static));
585        }
586    }
587
588    let self_ident = &input.ident;
589    let mut seen_msgpack = std::collections::HashSet::new();
590    for v in variants {
591        for entry in &v.payload {
592            let ty = entry.ty;
593            let key = quote!(#ty).to_string();
594            // Self-recursion handling: skip the `MsgpackTagged` bound for
595            // fields whose type contains the self-ident, and let the
596            // recursion call resolve co-inductively at the call site.
597            let self_typed = type_contains_ident(ty, self_ident);
598            if !self_typed && seen_msgpack.insert(key) {
599                where_clause.predicates.push(parse_quote!(#ty: ::msgpack_tagged::MsgpackTagged));
600            }
601        }
602        // Newtype variants don't go through the payload entries (their
603        // payload is empty), but their inner type still needs the
604        // `MsgpackTagged` bound so the recursive `register_into` call
605        // type-checks. Same self-recursion handling as above — drop the
606        // bound when the inner type is `Self`-typed and let the recursion
607        // call resolve co-inductively.
608        if let Some(ty) = v.newtype_inner {
609            let key = quote!(#ty).to_string();
610            let self_typed = type_contains_ident(ty, self_ident);
611            if !self_typed && seen_msgpack.insert(key) {
612                where_clause.predicates.push(parse_quote!(#ty: ::msgpack_tagged::MsgpackTagged));
613            }
614        }
615    }
616
617    for predicate in extra_bounds {
618        where_clause.predicates.push(predicate.clone());
619    }
620
621    Some(where_clause)
622}
623
624/// Where clause for newtype structs: every type param needs `'static` (from
625/// the supertrait), and the inner type must be `MsgpackTagged` so the
626/// `register_into` call type-checks. No field-type bounds beyond that — a
627/// newtype contributes no fields of its own to the wire.
628fn build_passthrough_where_clause(input: &DeriveInput, inner_type: &Type) -> Option<WhereClause> {
629    let mut where_clause = input.generics.where_clause.clone().unwrap_or_else(|| WhereClause {
630        where_token: <Token![where]>::default(),
631        predicates: Punctuated::new(),
632    });
633    for param in &input.generics.params {
634        if let GenericParam::Type(type_param) = param {
635            let ident = &type_param.ident;
636            where_clause.predicates.push(parse_quote!(#ident: 'static));
637        }
638    }
639    where_clause.predicates.push(parse_quote!(#inner_type: ::msgpack_tagged::MsgpackTagged));
640    Some(where_clause)
641}
642
643/// Reject any field-level `#[tag(...)]` attribute on the input. Used when
644/// `#[tagged(via(...))]` is set: the public type's fields are wire-irrelevant,
645/// so a `#[tag(...)]` annotation would either be a leftover from before the
646/// migration to `via` or a misunderstanding of where tags belong (on the
647/// wire DTO). Either way, loud rejection is better than silent ignore.
648fn validate_no_field_tag_attrs(input: &DeriveInput) -> syn::Result<()> {
649    let check = |fields: &Fields| -> syn::Result<()> {
650        for field in fields {
651            for attr in &field.attrs {
652                if attr.path().is_ident("tag") {
653                    return Err(syn::Error::new_spanned(
654                        attr,
655                        "field-level `#[tag(...)]` is not allowed on a type with `#[tagged(via(...))]` — \
656                         fields of a `via`-delegating type are wire-irrelevant; \
657                         tag the wire DTO's fields instead",
658                    ));
659                }
660            }
661        }
662        Ok(())
663    };
664    match &input.data {
665        Data::Struct(s) => check(&s.fields)?,
666        Data::Enum(e) => {
667            for variant in &e.variants {
668                for attr in &variant.attrs {
669                    if attr.path().is_ident("tag") {
670                        return Err(syn::Error::new_spanned(
671                            attr,
672                            "variant-level `#[tag(...)]` is not allowed on a type with `#[tagged(via(...))]` — \
673                             variants of a `via`-delegating enum are wire-irrelevant; \
674                             tag the wire DTO's variants instead",
675                        ));
676                    }
677                    if attr.path().is_ident("tagged") {
678                        return Err(syn::Error::new_spanned(
679                            attr,
680                            "variant-level `#[tagged(...)]` is not allowed on a type with `#[tagged(via(...))]` — \
681                             variants of a `via`-delegating enum are wire-irrelevant; \
682                             configure the wire DTO instead",
683                        ));
684                    }
685                }
686                check(&variant.fields)?;
687            }
688        }
689        Data::Union(u) => {
690            for field in &u.fields.named {
691                for attr in &field.attrs {
692                    if attr.path().is_ident("tag") {
693                        return Err(syn::Error::new_spanned(
694                            attr,
695                            "field-level `#[tag(...)]` is not allowed on a type with `#[tagged(via(...))]` — \
696                             fields of a `via`-delegating type are wire-irrelevant; \
697                             tag the wire DTO's fields instead",
698                        ));
699                    }
700                }
701            }
702        }
703    }
704    Ok(())
705}
706
707/// Stub expansion: empty `Tagged::Product`, no-op `register_into`. Used for
708/// shapes the macro hasn't learned to handle yet.
709fn stub(input: &DeriveInput) -> TokenStream2 {
710    let name = &input.ident;
711    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
712    let tagged = empty_product_literal();
713    quote! {
714        impl #impl_generics ::msgpack_tagged::MsgpackTagged for #name #ty_generics #where_clause {
715            const TAGGED: ::msgpack_tagged::Tagged = #tagged;
716            fn register_into(_reg: &mut ::msgpack_tagged::TagRegistry) {}
717        }
718    }
719}
720
721/// Per-tagged-field info collected during macro expansion. `name` is the
722/// field's wire-name as a string — for named structs that's the field
723/// identifier; for tuple structs it's the source-position-as-string ("0",
724/// "1", …). Either way, the name lands in the `Product`'s `fields` slice
725/// as a `&'static str` literal.
726struct TaggedField<'a> {
727    tag: u8,
728    name: String,
729    ty: &'a Type,
730}
731
732/// Parse a list of named fields (struct fields or named-variant payload
733/// fields) into the per-tagged-field entries that drive `Product` emission.
734/// Every field needs an explicit `#[tag(N)]` or auto-skips via `#[tag(skip)]`
735/// / `PhantomData<_>`; missing both is a compile error. The returned vec is
736/// already in tag-ascending order, the canonical wire order.
737fn parse_named_fields<'a>(
738    fields: &'a Punctuated<Field, Token![,]>,
739    reserved: &[u8],
740) -> syn::Result<(Vec<TaggedField<'a>>, bool)> {
741    let mut entries = Vec::with_capacity(fields.len());
742    let mut seen_tags = std::collections::HashSet::new();
743    for field in fields {
744        let ident = field.ident.as_ref().expect("named field has an ident");
745        match classify_field(field, reserved)? {
746            FieldKind::Tagged(tag) => {
747                if !seen_tags.insert(tag) {
748                    return Err(syn::Error::new_spanned(
749                        field,
750                        format!("tag {tag} is used more than once"),
751                    ));
752                }
753                // Field-level `#[serde(rename = "X")]` overrides the wire
754                // name. This is what makes the shadow-DTO pattern work when
755                // the wire DTO uses a different field name than the public
756                // type — `serialize_field("X", ...)` matches our `tag_for("X")`.
757                let wire_name =
758                    parse_serde_field_rename(field)?.unwrap_or_else(|| ident.to_string());
759                entries.push(TaggedField { tag, name: wire_name, ty: &field.ty });
760            }
761            FieldKind::Skipped => {}
762        }
763    }
764    // Compute the "source order is already tag-ascending" flag *before* the
765    // sort below — `entries` is currently in source-declaration order.
766    let tag_order_matches_source = is_tag_ascending(&entries);
767    entries.sort_by_key(|e| e.tag);
768    Ok((entries, tag_order_matches_source))
769}
770
771/// Whether `entries` (in source-declaration order) are already in
772/// tag-ascending order. Tags are unique within a Product (validated
773/// elsewhere) so this is equivalent to strict monotonicity.
774fn is_tag_ascending(entries: &[TaggedField<'_>]) -> bool {
775    entries.windows(2).all(|w| w[0].tag < w[1].tag)
776}
777
778/// Parse a list of unnamed (positional) fields — top-level tuple-struct
779/// fields or tuple-variant payload fields. Tagging style must be uniform:
780/// either every field carries `#[tag(N)]` (explicit, allows reordering /
781/// `default`) or none do (implicit positional 0, 1, 2, …). Mixing is
782/// rejected. The returned vec is in tag-ascending order with names being
783/// the position-as-string ("0", "1", …).
784///
785/// `mixing_error_span` controls where the "mixing implicit and explicit
786/// is rejected" error is anchored — typically the surrounding `DeriveInput`
787/// for top-level tuple structs or the variant for variant payloads.
788fn parse_tuple_fields<'a>(
789    mixing_error_span: &dyn ToTokens,
790    fields: &'a Punctuated<Field, Token![,]>,
791    reserved: &[u8],
792) -> syn::Result<(Vec<TaggedField<'a>>, bool)> {
793    let explicit_count =
794        fields.iter().filter(|f| f.attrs.iter().any(|a| a.path().is_ident("tag"))).count();
795    if explicit_count != 0 && explicit_count != fields.len() {
796        return Err(syn::Error::new_spanned(
797            mixing_error_span,
798            "tuple-style fields must either all carry `#[tag(N)]` or none — \
799             mixing implicit positional tags with explicit tags is rejected",
800        ));
801    }
802    let all_explicit = explicit_count == fields.len();
803
804    let mut entries = Vec::with_capacity(fields.len());
805    let mut seen_tags = std::collections::HashSet::new();
806    for (position, field) in fields.iter().enumerate() {
807        let position_u8: u8 = position.try_into().map_err(|_| {
808            syn::Error::new_spanned(
809                field,
810                format!("tuple position {position} is out of range for u8 tags"),
811            )
812        })?;
813        let tag = if all_explicit {
814            match classify_field(field, reserved)? {
815                FieldKind::Tagged(tag) => tag,
816                FieldKind::Skipped => {
817                    return Err(syn::Error::new_spanned(
818                        field,
819                        "`#[serde(skip)]` on tuple-style fields is not supported — \
820                         it would shift positional indices",
821                    ));
822                }
823            }
824        } else {
825            // Implicit positional: `#[serde(skip)]` would shift positional
826            // indices, so reject it instead of silently honoring it.
827            if has_serde_skip(field)? {
828                return Err(syn::Error::new_spanned(
829                    field,
830                    "`#[serde(skip)]` on tuple-style fields is not supported",
831                ));
832            }
833            if reserved.contains(&position_u8) {
834                return Err(syn::Error::new_spanned(
835                    field,
836                    format!(
837                        "implicit positional tag {position_u8} collides with the type's \
838                         `#[tagged(reserved(...))]` list — assign explicit `#[tag(N)]`s, \
839                         or remove the reserved entry"
840                    ),
841                ));
842            }
843            position_u8
844        };
845        if !seen_tags.insert(tag) {
846            return Err(syn::Error::new_spanned(
847                field,
848                format!("tag {tag} is used more than once"),
849            ));
850        }
851        entries.push(TaggedField { tag, name: position.to_string(), ty: &field.ty });
852    }
853    let tag_order_matches_source = is_tag_ascending(&entries);
854    entries.sort_by_key(|e| e.tag);
855    Ok((entries, tag_order_matches_source))
856}
857
858fn expand_named_struct(
859    input: &DeriveInput,
860    fields: &Punctuated<Field, Token![,]>,
861    type_attrs: &TypeAttrs,
862) -> syn::Result<TokenStream2> {
863    let name = &input.ident;
864    // The registry key is the *serde* name — it must match what
865    // `serialize_struct(name, ...)` will pass at runtime. So we honor
866    // `#[serde(rename = "...")]` if present, fall back to the Rust ident
867    // otherwise. This is what makes the shadow-DTO pattern work: the wire
868    // DTO `MemOpWire` with `#[serde(rename = "MemOp")]` registers under
869    // `"MemOp"`, and the wrapper's lookup at `serialize_struct("MemOp", ...)`
870    // hits correctly.
871    let name_str = parse_serde_rename(input)?.unwrap_or_else(|| name.to_string());
872
873    // `via` is handled in `expand` before dispatch — by the time we reach
874    // this function, it must be `None`. Reservation list and unknown-tag
875    // policy come from the already-parsed type attrs.
876    debug_assert!(type_attrs.via.is_none());
877    let reserved = &type_attrs.reserved;
878    let allow_unknown_tags = type_attrs.allow_unknown_tags;
879
880    // Parse each field. Tagged fields contribute to TAGS, the recursion list,
881    // and the where clause. Skipped fields (`#[tag(skip)]` or `PhantomData<_>`)
882    // are silently dropped — they don't go on the wire and don't constrain
883    // their type.
884    let (entries, tag_order_matches_source) = parse_named_fields(fields, reserved)?;
885
886    let recursion_calls = entries.iter().map(|e| {
887        let ty = e.ty;
888        quote! { <#ty as ::msgpack_tagged::MsgpackTagged>::register_into(_reg); }
889    });
890
891    // Bound *each tagged field's type* (rather than each generic param) on
892    // `MsgpackTagged`. This composes correctly with hand-written impls that
893    // have unusual bounds: e.g. if `MyType<A, B>: MsgpackTagged` requires
894    // `A: SomeOtherTrait`, our `where MyType<A, B>: MsgpackTagged` propagates
895    // that requirement through to the caller without us having to know about
896    // it. Naive per-type-param bounds (`A: MsgpackTagged, B: MsgpackTagged`)
897    // would be both too restrictive and insufficient in that case.
898    let tagged = product_literal(&entries, reserved, allow_unknown_tags, tag_order_matches_source);
899    let where_clause = build_where_clause(input, &entries, &type_attrs.extra_bounds);
900    let (impl_generics, ty_generics, _) = input.generics.split_for_impl();
901
902    Ok(quote! {
903        impl #impl_generics ::msgpack_tagged::MsgpackTagged for #name #ty_generics #where_clause {
904            const TAGGED: ::msgpack_tagged::Tagged = #tagged;
905
906            fn register_into(_reg: &mut ::msgpack_tagged::TagRegistry) {
907                if _reg.try_insert::<Self>(#name_str) {
908                    #(#recursion_calls)*
909                }
910            }
911        }
912    })
913}
914
915/// Expand the `#[tagged(via(WireType))]` form: the public type delegates
916/// `register_into` entirely to the wire DTO and contributes no entry of its
917/// own. The emitted `TAGGED` is an empty `Tagged::Product` purely to
918/// satisfy the trait — it's never consulted, because the public type itself
919/// never appears in the registry.
920fn expand_via(input: &DeriveInput, wire_type: &Type) -> TokenStream2 {
921    let name = &input.ident;
922    let where_clause = build_via_where_clause(input, wire_type);
923    let (impl_generics, ty_generics, _) = input.generics.split_for_impl();
924    let tagged = empty_product_literal();
925
926    quote! {
927        impl #impl_generics ::msgpack_tagged::MsgpackTagged for #name #ty_generics #where_clause {
928            const TAGGED: ::msgpack_tagged::Tagged = #tagged;
929
930            fn register_into(_reg: &mut ::msgpack_tagged::TagRegistry) {
931                <#wire_type as ::msgpack_tagged::MsgpackTagged>::register_into(_reg);
932            }
933        }
934    }
935}
936
937/// Build the where clause for a `via`-delegating impl. The public type
938/// contributes no field-type bounds (it has no field types on the wire), but
939/// it does need:
940/// 1. `T: 'static` on every type parameter (the supertrait propagates `Self: 'static`).
941/// 2. `<WireType>: MsgpackTagged` so the recursive call type-checks.
942fn build_via_where_clause(input: &DeriveInput, wire_type: &Type) -> Option<WhereClause> {
943    let mut where_clause = input.generics.where_clause.clone().unwrap_or_else(|| WhereClause {
944        where_token: <Token![where]>::default(),
945        predicates: Punctuated::new(),
946    });
947    for param in &input.generics.params {
948        if let GenericParam::Type(type_param) = param {
949            let ident = &type_param.ident;
950            where_clause.predicates.push(parse_quote!(#ident: 'static));
951        }
952    }
953    where_clause.predicates.push(parse_quote!(#wire_type: ::msgpack_tagged::MsgpackTagged));
954    Some(where_clause)
955}
956
957/// What the macro should do with a given field on the wire.
958enum FieldKind {
959    /// Field appears on the wire under integer tag `tag`.
960    Tagged(u8),
961    /// Field is omitted from the wire (via explicit `#[tag(skip)]` or because
962    /// its type is `PhantomData<_>`). Skipped fields contribute no entry to
963    /// `TAGS`, no recursion into `register_into`, and no where-clause bound.
964    Skipped,
965}
966
967/// Inner-args grammar for `#[tag(...)]`: a single integer tag literal
968/// (`#[tag(N)]`). Wire-tolerance for missing tags and field-skipping are
969/// expressed via serde-derive's `#[serde(default)]` / `#[serde(skip)]`
970/// (the latter is auto-recognized as the canonical skip signal).
971struct TagArgs(u8);
972
973impl Parse for TagArgs {
974    fn parse(input: ParseStream) -> syn::Result<Self> {
975        let lit: LitInt = input.parse()?;
976        let tag: u8 = lit.base10_parse()?;
977        if !input.is_empty() {
978            return Err(input.error("`#[tag(...)]` accepts a single integer tag literal"));
979        }
980        Ok(TagArgs(tag))
981    }
982}
983
984/// Decide whether a field is wire-visible or skipped. Errors loudly when a
985/// field has no annotation and isn't a recognized auto-skip type — the
986/// strict-by-default discipline. Also enforces that an active `#[tag(N)]`
987/// doesn't collide with the surrounding `#[tagged(reserved(...))]` list,
988/// and that `#[tag(N)]` and `#[serde(skip)]` aren't both set on the same
989/// field (those are contradictory — one says "on the wire", the other
990/// "not on the wire").
991fn classify_field(field: &Field, reserved: &[u8]) -> syn::Result<FieldKind> {
992    let serde_skip = has_serde_skip(field)?;
993    let mut found: Option<(&Attribute, TagArgs)> = None;
994    for attr in &field.attrs {
995        if !attr.path().is_ident("tag") {
996            continue;
997        }
998        if found.is_some() {
999            return Err(syn::Error::new_spanned(attr, "duplicate `#[tag(...)]` attribute"));
1000        }
1001        found = Some((attr, attr.parse_args()?));
1002    }
1003
1004    // Explicit annotation wins over auto-skip — if the user explicitly tags a
1005    // PhantomData field with `#[tag(N)]`, we honor that (unusual but valid).
1006    if let Some((attr, TagArgs(tag))) = found {
1007        if serde_skip {
1008            return Err(syn::Error::new_spanned(
1009                attr,
1010                "field has both `#[tag(N)]` and `#[serde(skip)]` — these are \
1011                 contradictory; pick one (`#[serde(skip)]` to drop the field, \
1012                 or `#[tag(N)]` to put the field on the wire under tag N)",
1013            ));
1014        }
1015        if reserved.contains(&tag) {
1016            return Err(syn::Error::new_spanned(
1017                attr,
1018                format!(
1019                    "tag {tag} is in the surrounding `#[tagged(reserved(...))]` list — pick a different tag, or remove it from the reserved list"
1020                ),
1021            ));
1022        }
1023        return Ok(FieldKind::Tagged(tag));
1024    }
1025
1026    // No `#[tag(...)]` at all — `#[serde(skip)]` drops the field from the
1027    // wire; `PhantomData<_>` is auto-skipped (the conventional zero-sized
1028    // "use a type parameter without storing anything" pattern). Any other
1029    // untagged field is an error.
1030    if serde_skip {
1031        return Ok(FieldKind::Skipped);
1032    }
1033    if is_phantom_data(&field.ty) {
1034        return Ok(FieldKind::Skipped);
1035    }
1036
1037    Err(syn::Error::new_spanned(
1038        field,
1039        "missing `#[tag(N)]` attribute — every field needs an explicit tag, \
1040         `#[serde(skip)]`, or be `PhantomData<_>`",
1041    ))
1042}
1043
1044/// Read `#[serde(rename = "X")]` off a list of attributes, if present, and
1045/// return `"X"`. Used both at the type level (the returned name becomes the
1046/// registry key) and at the field level (the returned name becomes the
1047/// `Product.fields` wire-name for that field).
1048///
1049/// Only the simple symmetric form `rename = "X"` is recognized. Other serde
1050/// items (`default`, `skip`, `rename_all`, asymmetric `rename(serialize = ...,
1051/// deserialize = ...)`, etc.) are ignored. If the user has multiple
1052/// `#[serde(rename = "X")]` attributes that disagree, the last one wins
1053/// (matches serde's own behavior).
1054fn parse_serde_rename_in_attrs(attrs: &[Attribute]) -> syn::Result<Option<String>> {
1055    let mut found: Option<String> = None;
1056    for attr in attrs {
1057        if !attr.path().is_ident("serde") {
1058            continue;
1059        }
1060        let items: Punctuated<Meta, Token![,]> =
1061            attr.parse_args_with(Punctuated::parse_terminated)?;
1062        for item in items {
1063            if let Meta::NameValue(nv) = &item
1064                && nv.path.is_ident("rename")
1065                && let Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) = &nv.value
1066            {
1067                found = Some(s.value());
1068            }
1069        }
1070    }
1071    Ok(found)
1072}
1073
1074/// Type-level `#[serde(rename = "X")]` — used as the registry key for a
1075/// type, so it matches what `serialize_struct(name, ...)` passes at runtime
1076/// through the auto-derived `Serialize` impl.
1077fn parse_serde_rename(input: &DeriveInput) -> syn::Result<Option<String>> {
1078    parse_serde_rename_in_attrs(&input.attrs)
1079}
1080
1081/// Field-level `#[serde(rename = "X")]` — used as the wire-name in
1082/// `Product.fields` for that field, matching what `serialize_field("X", ...)`
1083/// passes at runtime through the auto-derived `Serialize` impl. The
1084/// load-bearing piece for the shadow-DTO pattern when the wire DTO renames
1085/// individual fields (e.g., `index` → `i`).
1086fn parse_serde_field_rename(field: &Field) -> syn::Result<Option<String>> {
1087    parse_serde_rename_in_attrs(&field.attrs)
1088}
1089
1090/// Whether a field carries `#[serde(skip)]` — recognized by the macro as an
1091/// alias for `#[tag(skip)]`. Only the bare-ident form is honored;
1092/// asymmetric `skip_serializing` / `skip_deserializing` and conditional
1093/// `skip_serializing_if = "..."` are deliberately ignored, since they don't
1094/// have a clean encode-and-decode-symmetric mapping in this format.
1095fn has_serde_skip(field: &Field) -> syn::Result<bool> {
1096    for attr in &field.attrs {
1097        if !attr.path().is_ident("serde") {
1098            continue;
1099        }
1100        let items: Punctuated<Meta, Token![,]> =
1101            attr.parse_args_with(Punctuated::parse_terminated)?;
1102        for item in items {
1103            if let Meta::Path(path) = &item
1104                && path.is_ident("skip")
1105            {
1106                return Ok(true);
1107            }
1108        }
1109    }
1110    Ok(false)
1111}
1112
1113/// Variant-level configuration parsed from one or more `#[tagged(...)]`
1114/// attributes on an enum variant. Two grammar groups apply:
1115/// * **Payload-shape modifiers** — `reserved(...)` and `allow_unknown_tags`
1116///   configure the variant's payload (shape-equivalent to a struct).
1117/// * **Fallback markers** — `on_reserved` and `on_unknown` mark this variant
1118///   as the routing target for retired and unknown wire tags respectively,
1119///   on the enclosing enum. Restricted to unit variants (validated by the
1120///   caller — we can see the variant fields there but not here).
1121#[derive(Default)]
1122struct VariantAttrs {
1123    reserved: Vec<u8>,
1124    allow_unknown_tags: bool,
1125    on_reserved: bool,
1126    on_unknown: bool,
1127}
1128
1129/// Parse the variant-level `#[tagged(...)]` attributes (if any) into a
1130/// `VariantAttrs`. Multiple `#[tagged(...)]` attributes on the same variant
1131/// are allowed and merged, but each named modifier may appear at most once
1132/// across them.
1133fn parse_tagged_variant_attrs(variant: &Variant) -> syn::Result<VariantAttrs> {
1134    let mut out = VariantAttrs::default();
1135
1136    for attr in &variant.attrs {
1137        if !attr.path().is_ident("tagged") {
1138            continue;
1139        }
1140        let items: Punctuated<Meta, Token![,]> =
1141            attr.parse_args_with(Punctuated::parse_terminated)?;
1142        for item in items {
1143            if let Meta::List(list) = &item
1144                && list.path.is_ident("reserved")
1145            {
1146                let lits: Punctuated<LitInt, Token![,]> =
1147                    list.parse_args_with(Punctuated::parse_terminated)?;
1148                for lit in &lits {
1149                    let n: u8 = lit.base10_parse()?;
1150                    if out.reserved.contains(&n) {
1151                        return Err(syn::Error::new_spanned(
1152                            lit,
1153                            format!("tag {n} listed more than once in `reserved(...)`"),
1154                        ));
1155                    }
1156                    out.reserved.push(n);
1157                }
1158                continue;
1159            }
1160            if let Meta::Path(path) = &item
1161                && path.is_ident("allow_unknown_tags")
1162            {
1163                if out.allow_unknown_tags {
1164                    return Err(syn::Error::new_spanned(
1165                        path,
1166                        "duplicate `allow_unknown_tags` modifier in `#[tagged(...)]`",
1167                    ));
1168                }
1169                out.allow_unknown_tags = true;
1170                continue;
1171            }
1172            if let Meta::Path(path) = &item
1173                && path.is_ident("on_reserved")
1174            {
1175                if out.on_reserved {
1176                    return Err(syn::Error::new_spanned(
1177                        path,
1178                        "duplicate `on_reserved` modifier in `#[tagged(...)]`",
1179                    ));
1180                }
1181                out.on_reserved = true;
1182                continue;
1183            }
1184            if let Meta::Path(path) = &item
1185                && path.is_ident("on_unknown")
1186            {
1187                if out.on_unknown {
1188                    return Err(syn::Error::new_spanned(
1189                        path,
1190                        "duplicate `on_unknown` modifier in `#[tagged(...)]`",
1191                    ));
1192                }
1193                out.on_unknown = true;
1194                continue;
1195            }
1196            return Err(syn::Error::new_spanned(
1197                &item,
1198                "expected `reserved(...)`, `allow_unknown_tags`, `on_reserved`, or \
1199                 `on_unknown` inside `#[tagged(...)]` on an enum variant — \
1200                 `via(...)` is a type-level modifier, not variant-level",
1201            ));
1202        }
1203    }
1204    Ok(out)
1205}
1206
1207/// Type-level configuration parsed from one or more `#[tagged(...)]`
1208/// attributes on the struct/enum. Holds every modifier the macro understands
1209/// at the type level.
1210#[derive(Default)]
1211struct TypeAttrs {
1212    /// Tags listed in `#[tagged(reserved(N, M, ...))]`. Empty if absent.
1213    reserved: Vec<u8>,
1214    /// `true` iff `#[tagged(allow_unknown_tags)]` appears anywhere. Applies
1215    /// to product (struct) shapes only — sums reject it (no skip semantics
1216    /// for an unknown variant tag).
1217    allow_unknown_tags: bool,
1218    /// The wire DTO from `#[tagged(via(WireType))]`, if present. When set,
1219    /// the public type delegates `register_into` to this type and contributes
1220    /// no entry of its own to the registry. Mutually exclusive with every
1221    /// other type-level modifier — those are wire-format properties and
1222    /// belong on the wire DTO.
1223    via: Option<Type>,
1224    /// Extra where-clause predicates from one or more
1225    /// `#[tagged(extra_bound = "...")]` attributes. The string is parsed as
1226    /// a comma-separated list of where-predicates and appended verbatim to
1227    /// the impl's where clause. Used to restore bounds the macro can't infer
1228    /// — most commonly to put back a sibling type's `MsgpackTagged` bound
1229    /// after the self-filter has dropped the bound on a self-recursive field
1230    /// like `Vec<(Other, Self)>` (the recursion call still needs
1231    /// `Other: MsgpackTagged` to type-check).
1232    extra_bounds: Vec<WherePredicate>,
1233}
1234
1235/// Parse the type-level `#[tagged(...)]` attributes (if any) into a single
1236/// `TypeAttrs`. Multiple `#[tagged(...)]` attributes are allowed and merged,
1237/// but each named modifier may appear at most once across them.
1238///
1239/// Inner grammar — comma-separated items, each one of:
1240/// * `reserved(N, M, ...)` — list-form, integer literals, no duplicates.
1241/// * `allow_unknown_tags` — bare ident, presence-only. Product-shapes only.
1242/// * `via(WireType)` — list-form, single Rust type (the wire DTO to delegate
1243///   `register_into` to). Mutually exclusive with every other modifier —
1244///   those properties belong on the wire DTO.
1245/// * `extra_bound = "..."` — string-form, parsed as a comma-separated list
1246///   of where-predicates appended to the impl's where clause. Escape hatch
1247///   for bounds the macro can't infer — typically to restore a sibling
1248///   type's `MsgpackTagged` bound after the self-filter has dropped a
1249///   bound on a self-recursive field like `Vec<(Other, Self)>`.
1250fn parse_tagged_type_attrs(input: &DeriveInput) -> syn::Result<TypeAttrs> {
1251    let mut out = TypeAttrs::default();
1252
1253    for attr in &input.attrs {
1254        if !attr.path().is_ident("tagged") {
1255            continue;
1256        }
1257        let items: Punctuated<Meta, Token![,]> =
1258            attr.parse_args_with(Punctuated::parse_terminated)?;
1259        for item in items {
1260            if let Meta::List(list) = &item
1261                && list.path.is_ident("reserved")
1262            {
1263                let lits: Punctuated<LitInt, Token![,]> =
1264                    list.parse_args_with(Punctuated::parse_terminated)?;
1265                for lit in &lits {
1266                    let n: u8 = lit.base10_parse()?;
1267                    if out.reserved.contains(&n) {
1268                        return Err(syn::Error::new_spanned(
1269                            lit,
1270                            format!("tag {n} listed more than once in `reserved(...)`"),
1271                        ));
1272                    }
1273                    out.reserved.push(n);
1274                }
1275                continue;
1276            }
1277            if let Meta::Path(path) = &item
1278                && path.is_ident("allow_unknown_tags")
1279            {
1280                if out.allow_unknown_tags {
1281                    return Err(syn::Error::new_spanned(
1282                        path,
1283                        "duplicate `allow_unknown_tags` modifier in `#[tagged(...)]`",
1284                    ));
1285                }
1286                out.allow_unknown_tags = true;
1287                continue;
1288            }
1289            if let Meta::List(list) = &item
1290                && list.path.is_ident("via")
1291            {
1292                if out.via.is_some() {
1293                    return Err(syn::Error::new_spanned(
1294                        list,
1295                        "duplicate `via(...)` modifier in `#[tagged(...)]`",
1296                    ));
1297                }
1298                out.via = Some(list.parse_args::<Type>()?);
1299                continue;
1300            }
1301            if let Meta::NameValue(nv) = &item
1302                && nv.path.is_ident("extra_bound")
1303            {
1304                // Multiple `extra_bound = "..."` items accumulate — each
1305                // string contributes its predicates to the impl's where
1306                // clause. No duplicate-detection: extra_bound is purely
1307                // additive and there's no harm in repeating identical
1308                // bounds (the where clause is set-like at the language level).
1309                let Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) = &nv.value else {
1310                    return Err(syn::Error::new_spanned(
1311                        &nv.value,
1312                        "`extra_bound` requires a string literal of the form \
1313                         `\"T: Trait, U: Trait\"`",
1314                    ));
1315                };
1316                let bound_str = s.value();
1317                // Parse as a where clause and steal its predicates. The
1318                // explicit `where` keyword lets us reuse syn's WhereClause
1319                // parser; without it, syn doesn't expose a public path for
1320                // parsing comma-separated WherePredicate lists directly.
1321                let where_clause: WhereClause = syn::parse_str(&format!("where {bound_str}"))
1322                    .map_err(|e| {
1323                        syn::Error::new_spanned(
1324                            s,
1325                            format!("failed to parse `extra_bound` predicates: {e}"),
1326                        )
1327                    })?;
1328                out.extra_bounds.extend(where_clause.predicates);
1329                continue;
1330            }
1331            return Err(syn::Error::new_spanned(
1332                &item,
1333                "expected `reserved(...)`, `allow_unknown_tags`, `via(...)`, or \
1334                 `extra_bound = \"...\"` inside `#[tagged(...)]` on a type",
1335            ));
1336        }
1337    }
1338
1339    // `via` is wire-format-agnostic delegation — the wire DTO carries every
1340    // wire-format property, the public type carries none.
1341    if out.via.is_some() {
1342        if !out.reserved.is_empty() {
1343            return Err(syn::Error::new_spanned(
1344                input,
1345                "`#[tagged(via(...))]` is incompatible with `reserved(...)` — \
1346                 the reserved-tag list belongs on the wire DTO, not on the public type",
1347            ));
1348        }
1349        if out.allow_unknown_tags {
1350            return Err(syn::Error::new_spanned(
1351                input,
1352                "`#[tagged(via(...))]` is incompatible with `allow_unknown_tags` — \
1353                 that flag belongs on the wire DTO, not on the public type",
1354            ));
1355        }
1356        if !out.extra_bounds.is_empty() {
1357            return Err(syn::Error::new_spanned(
1358                input,
1359                "`#[tagged(via(...))]` is incompatible with `extra_bound = \"...\"` — \
1360                 the public type's where clause is just the delegation glue; \
1361                 if a custom bound is needed, put it on the wire DTO",
1362            ));
1363        }
1364    }
1365
1366    Ok(out)
1367}
1368
1369/// Syntactically detect `PhantomData<_>` by checking the last path segment.
1370/// Matches the conventional forms (`PhantomData`, `marker::PhantomData`,
1371/// `std::marker::PhantomData`, `core::marker::PhantomData`). Won't recognize
1372/// a `PhantomData` re-imported under a different alias — that's the standard
1373/// trade-off for syntactic detection (serde-derive's auto-skip works the same way).
1374fn is_phantom_data(ty: &Type) -> bool {
1375    if let Type::Path(type_path) = ty
1376        && let Some(last) = type_path.path.segments.last()
1377    {
1378        return last.ident == "PhantomData";
1379    }
1380    false
1381}
1382
1383/// Walk a type's AST and check whether the impl's self-type ident appears
1384/// anywhere inside it. Used to detect self-recursive tagged fields like
1385/// `children: Vec<Self>` in `enum Tree { ... Vec<Tree> ... }`.
1386///
1387/// We use this to *skip* emitting the `<FieldType>: MsgpackTagged` bound for
1388/// such fields — that bound triggers a co-inductive cycle in Rust's trait
1389/// solver (`Vec<Tree>: MsgpackTagged` → `Tree: MsgpackTagged` → which is the
1390/// impl we're defining → recursion-limit overflow). We *don't* skip the
1391/// recursion call inside `register_into`: at the call site, Rust resolves
1392/// `Vec<Tree>: MsgpackTagged` co-inductively against the current impl, which
1393/// works fine — only the impl-validity-time check chokes on the cycle. The
1394/// `try_insert` short-circuit makes the runtime self-recursion a no-op.
1395///
1396/// The catch: for a field like `Vec<(Other, Self)>`, dropping the bound is
1397/// still safe (Other's bound chases via the call-site path), but if no other
1398/// impl provides `Other: MsgpackTagged` the user gets a clear compile-time
1399/// error pointing at the field. They restore the bound via
1400/// `#[tagged(extra_bound = "Other: MsgpackTagged")]`.
1401///
1402/// Detection is purely syntactic — anywhere the self-ident appears as a path
1403/// segment counts. Won't catch type aliases that resolve to Self, or types
1404/// re-imported under a different name; those edge cases need a hand-written
1405/// impl, same as more complex self-recursion shapes.
1406fn type_contains_ident(ty: &Type, target: &Ident) -> bool {
1407    match ty {
1408        Type::Path(p) => {
1409            for seg in &p.path.segments {
1410                if &seg.ident == target {
1411                    return true;
1412                }
1413                if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
1414                    for arg in &args.args {
1415                        if let syn::GenericArgument::Type(inner) = arg
1416                            && type_contains_ident(inner, target)
1417                        {
1418                            return true;
1419                        }
1420                    }
1421                }
1422            }
1423            false
1424        }
1425        Type::Reference(r) => type_contains_ident(&r.elem, target),
1426        Type::Array(a) => type_contains_ident(&a.elem, target),
1427        Type::Slice(s) => type_contains_ident(&s.elem, target),
1428        Type::Tuple(t) => t.elems.iter().any(|e| type_contains_ident(e, target)),
1429        Type::Paren(p) => type_contains_ident(&p.elem, target),
1430        Type::Group(g) => type_contains_ident(&g.elem, target),
1431        Type::Ptr(p) => type_contains_ident(&p.elem, target),
1432        _ => false,
1433    }
1434}
1435
1436/// Build a `where` clause for the generated impl. Adds three kinds of bounds:
1437///
1438/// 1. **`T: 'static` for every type parameter on the input.** The
1439///    `MsgpackTagged: 'static` supertrait propagates `Self: 'static` onto the
1440///    impl, which requires every generic param to be `'static` regardless of
1441///    whether it appears in a tagged field. (Skipped fields like
1442///    `_phantom: PhantomData<T>` still reference T at the type level, so
1443///    `Self: 'static` requires `T: 'static` even though we don't tag the
1444///    PhantomData field.)
1445/// 2. **`<TaggedFieldType>: MsgpackTagged` for each tagged field's type.**
1446///    Per-field-type bounds compose with hand-written impls that have unusual
1447///    bounds: if `MyType<A, B>: MsgpackTagged` requires `A: SomeOtherTrait`,
1448///    our `where MyType<A, B>: MsgpackTagged` propagates that requirement to
1449///    the caller transparently. Field types appearing more than once are only
1450///    emitted as a bound once.
1451///
1452/// Returns `None` only if the input has no generic params, no tagged fields,
1453/// and no pre-existing where clause — that lets the caller avoid emitting a
1454/// stray `where` token.
1455fn build_where_clause(
1456    input: &DeriveInput,
1457    entries: &[TaggedField<'_>],
1458    extra_bounds: &[WherePredicate],
1459) -> Option<WhereClause> {
1460    let has_type_params = input.generics.params.iter().any(|p| matches!(p, GenericParam::Type(_)));
1461    if entries.is_empty() && !has_type_params && extra_bounds.is_empty() {
1462        return input.generics.where_clause.clone();
1463    }
1464
1465    let mut where_clause = input.generics.where_clause.clone().unwrap_or_else(|| WhereClause {
1466        where_token: <Token![where]>::default(),
1467        predicates: Punctuated::new(),
1468    });
1469
1470    for param in &input.generics.params {
1471        if let GenericParam::Type(type_param) = param {
1472            let ident = &type_param.ident;
1473            where_clause.predicates.push(parse_quote!(#ident: 'static));
1474        }
1475    }
1476
1477    let self_ident = &input.ident;
1478    let mut seen_tagged = std::collections::HashSet::new();
1479    for entry in entries {
1480        let ty = entry.ty;
1481        // Dedup by stringified token-stream of the type. Not semantic equality
1482        // (`Vec<u32>` vs `std::vec::Vec<u32>` would be treated as distinct),
1483        // but it dedupes the common case where the same path is written the
1484        // same way in multiple field declarations.
1485        let key = quote!(#ty).to_string();
1486        // Self-typed field types (e.g. `Vec<Self>` in a recursive enum) skip
1487        // the `MsgpackTagged` bound to dodge the trait-solver cycle. The
1488        // recursion call inside `register_into` is still emitted; Rust's
1489        // call-site resolution accepts the co-inductive cycle.
1490        let self_typed = type_contains_ident(ty, self_ident);
1491        if !self_typed && seen_tagged.insert(key) {
1492            where_clause.predicates.push(parse_quote!(#ty: ::msgpack_tagged::MsgpackTagged));
1493        }
1494    }
1495    for predicate in extra_bounds {
1496        where_clause.predicates.push(predicate.clone());
1497    }
1498    Some(where_clause)
1499}