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