Skip to main content

wowlab_engine_macros_impl/
codegen.rs

1use quote::quote;
2use syn::{
3    Data, DeriveInput, Field, Fields, Ident, Type, Visibility, ext::IdentExt,
4    punctuated::Punctuated, token::Comma,
5};
6
7use crate::parse::{ExprAttr, SlotAttr, parse_named_field_attr, parse_trait_crate_attr};
8
9const OFFSET_IDENT_CAPACITY: usize = 32;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12enum DescriptorFieldAbi {
13    Float,
14    Int,
15}
16
17impl DescriptorFieldAbi {
18    const fn rust_type_name(self) -> &'static str {
19        match self {
20            Self::Float => "f64",
21            Self::Int => "i32",
22        }
23    }
24}
25
26fn default_dbc_support_path() -> syn::Path {
27    syn::parse_quote!(crate::parsers::dbc::macro_support)
28}
29
30fn dbc_trait_path(support_path: &syn::Path, trait_name: &str) -> proc_macro2::TokenStream {
31    let trait_ident = Ident::new(trait_name, proc_macro2::Span::call_site());
32
33    quote! { #support_path::#trait_ident }
34}
35
36// Syntactic last-segment check: a type alias to f64 is missed, but that only affects dump_fields formatting, not the ABI.
37fn is_f64(ty: &Type) -> bool {
38    let Type::Path(type_path) = ty else {
39        return false;
40    };
41
42    type_path
43        .path
44        .segments
45        .last()
46        .is_some_and(|seg| seg.ident == "f64" && seg.arguments.is_none())
47}
48
49fn primitive_descriptor_abi(ty: &Type) -> Option<DescriptorFieldAbi> {
50    let mut ty = ty;
51
52    loop {
53        match ty {
54            Type::Group(group) => ty = &group.elem,
55            Type::Paren(paren) => ty = &paren.elem,
56            _ => break,
57        }
58    }
59
60    let Type::Path(type_path) = ty else {
61        return None;
62    };
63    let segment = type_path.path.segments.last()?;
64
65    if !segment.arguments.is_none() {
66        return None;
67    }
68
69    match segment.ident.to_string().as_str() {
70        "f64" => Some(DescriptorFieldAbi::Float),
71        "i32" => Some(DescriptorFieldAbi::Int),
72        _ => None,
73    }
74}
75
76fn expected_descriptor_abi(expr: &ExprAttr) -> syn::Result<DescriptorFieldAbi> {
77    match expr.field_type_ident.to_string().as_str() {
78        "Float" => Ok(DescriptorFieldAbi::Float),
79        "Int" | "Bool" => Ok(DescriptorFieldAbi::Int),
80        other => Err(syn::Error::new_spanned(
81            &expr.field_type_ident,
82            format!(
83                "unsupported descriptor FieldType `{other}`; expected `Float`, `Int`, or `Bool`"
84            ),
85        )),
86    }
87}
88
89fn validate_descriptor_field_abi(expr: &ExprAttr, field_ty: &Type) -> syn::Result<()> {
90    let expected = expected_descriptor_abi(expr)?;
91    let declared = expr.field_type_ident.to_string();
92    let field_name = expr.field_ident.to_string();
93    let Some(actual) = primitive_descriptor_abi(field_ty) else {
94        return Err(syn::Error::new_spanned(
95            field_ty,
96            format!(
97                "field `{field_name}` uses unsupported Rust ABI type `{}` for descriptor FieldType \
98                 `{declared}`; expected `f64` for `Float` or `i32` for `Int`/`Bool`",
99                quote!(#field_ty)
100            ),
101        ));
102    };
103
104    if actual != expected {
105        return Err(syn::Error::new_spanned(
106            field_ty,
107            format!(
108                "descriptor FieldType `{declared}` for field `{field_name}` requires Rust ABI type \
109                 `{}`, found `{}`",
110                expected.rust_type_name(),
111                actual.rust_type_name(),
112            ),
113        ));
114    }
115
116    Ok(())
117}
118
119fn find_named_field<'a>(input: &'a DeriveInput, field_name: &Ident) -> syn::Result<&'a Field> {
120    let Data::Struct(data_struct) = &input.data else {
121        return Err(syn::Error::new_spanned(
122            &input.ident,
123            "this derive can only be applied to structs",
124        ));
125    };
126    let Fields::Named(named) = &data_struct.fields else {
127        return Err(syn::Error::new_spanned(
128            &input.ident,
129            "this derive requires a struct with named fields",
130        ));
131    };
132
133    named
134        .named
135        .iter()
136        .find(|f| f.ident.as_ref() == Some(field_name))
137        .ok_or_else(|| {
138            syn::Error::new_spanned(
139                field_name,
140                format!(
141                    "struct `{}` has no field named `{}`",
142                    input.ident, field_name
143                ),
144            )
145        })
146}
147
148pub(crate) fn expand_has_id(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
149    let name = &input.ident;
150    let field_ident =
151        parse_named_field_attr(input, "id_field")?.unwrap_or_else(|| Ident::new("ID", name.span()));
152    let _ = find_named_field(input, &field_ident)?;
153
154    let support_path =
155        parse_trait_crate_attr(input, "has_id")?.unwrap_or_else(default_dbc_support_path);
156    let trait_path = dbc_trait_path(&support_path, "HasId");
157    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
158
159    Ok(quote! {
160        impl #impl_generics #trait_path for #name #type_generics #where_clause {
161            fn id(&self) -> i32 {
162                self.#field_ident
163            }
164        }
165    })
166}
167
168pub(crate) fn expand_has_fk(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
169    let name = &input.ident;
170    let field_ident = parse_named_field_attr(input, "fk_field")?.ok_or_else(|| {
171        syn::Error::new_spanned(
172            name,
173            "#[derive(HasFk)] requires #[fk_field = \"FieldName\"]",
174        )
175    })?;
176    let _ = find_named_field(input, &field_ident)?;
177
178    let support_path =
179        parse_trait_crate_attr(input, "has_fk")?.unwrap_or_else(default_dbc_support_path);
180    let trait_path = dbc_trait_path(&support_path, "HasFk");
181    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
182
183    Ok(quote! {
184        impl #impl_generics #trait_path for #name #type_generics #where_clause {
185            fn fk(&self) -> i32 {
186                self.#field_ident
187            }
188        }
189    })
190}
191
192pub(crate) fn offset_const_name(field_name: &Ident) -> Ident {
193    let mut buf = String::with_capacity(OFFSET_IDENT_CAPACITY);
194
195    buf.push_str("OFF_");
196
197    for ch in field_name.unraw().to_string().chars() {
198        for upper in ch.to_uppercase() {
199            buf.push(upper);
200        }
201    }
202
203    Ident::new(&buf, field_name.span())
204}
205
206// #t(fn: rust_cyclomatic_complexity) validates a declarative slot grammar with independent attribute branches.
207// #t(fn: rust_collection_new_in_loop, rust_clone_in_loop) emitted fields must own per-field attribute tokens.
208pub(crate) fn expand_define_slot(
209    item: &syn::ItemStruct,
210    impl_body: Option<&proc_macro2::TokenStream>,
211) -> syn::Result<proc_macro2::TokenStream> {
212    let name = &item.ident;
213    let vis = &item.vis;
214
215    if !item.generics.params.is_empty() || item.generics.where_clause.is_some() {
216        return Err(syn::Error::new_spanned(
217            &item.generics,
218            "define_slot! does not support generic slot types because each registered slot must \
219             have one static size and layout",
220        ));
221    }
222
223    let mut slot_attr: Option<SlotAttr> = None;
224    let mut struct_attrs = Vec::with_capacity(item.attrs.len());
225
226    for attr in &item.attrs {
227        if attr.path().is_ident("slot") {
228            if slot_attr.is_some() {
229                return Err(syn::Error::new_spanned(
230                    attr,
231                    "duplicate #[slot(...)] attribute",
232                ));
233            }
234
235            slot_attr = Some(crate::parse::parse_slot_attr(attr)?);
236        } else if attr.path().is_ident("derive") {
237            return Err(syn::Error::new_spanned(
238                attr,
239                "define_slot! already injects #[derive(Clone, Copy, Debug, Default)]; \
240                 remove this #[derive(...)] (extra derives are not supported)",
241            ));
242        } else if attr.path().is_ident("repr") {
243            return Err(syn::Error::new_spanned(
244                attr,
245                "define_slot! owns #[repr(C)]; additional #[repr(...)] attributes are not supported",
246            ));
247        } else {
248            struct_attrs.push(attr);
249        }
250    }
251
252    let fields = match &item.fields {
253        Fields::Named(f) => &f.named,
254        other => {
255            return Err(syn::Error::new_spanned(
256                other,
257                "define_slot! requires a struct with named fields, got tuple or unit struct",
258            ));
259        }
260    };
261
262    let mut expr_attrs: Vec<ExprAttr> = Vec::new();
263    let mut stripped_fields: Punctuated<Field, Comma> = Punctuated::new();
264
265    for field in fields {
266        let field_ident = field
267            .ident
268            .as_ref()
269            .ok_or_else(|| syn::Error::new_spanned(field, "define_slot! fields must be named"))?;
270
271        let mut clean_attrs = Vec::with_capacity(field.attrs.len());
272
273        for attr in &field.attrs {
274            if attr.path().is_ident("expr") {
275                if slot_attr.is_none() {
276                    return Err(syn::Error::new_spanned(
277                        attr,
278                        "#[expr(...)] requires a #[slot(...)] attribute on the struct",
279                    ));
280                }
281
282                let expr = crate::parse::parse_expr_attr(attr, field_ident)?;
283
284                validate_descriptor_field_abi(&expr, &field.ty)?;
285                expr_attrs.push(expr);
286            } else {
287                clean_attrs.push(attr.clone());
288            }
289        }
290
291        let mut stripped = field.clone();
292
293        stripped.attrs = clean_attrs;
294        stripped_fields.push(stripped);
295    }
296
297    let field_items = build_field_items(name, fields)?;
298    let off_consts = &field_items.offset_consts;
299    let offset_idents = &field_items.offset_idents;
300    let field_sizes_aligns = &field_items.sizes_and_alignments;
301    let field_types = &field_items.field_types;
302    let dump_lines = build_dump_lines(name, fields)?;
303    let dump_fields_ident = Ident::new("dump_fields", name.span());
304
305    let impl_block = impl_body.map(|body| {
306        quote! {
307            impl #name {
308                #body
309            }
310        }
311    });
312
313    let submissions = if let Some(ref slot) = slot_attr {
314        build_submissions(name, slot, &expr_attrs)
315    } else {
316        Vec::new()
317    };
318
319    Ok(quote! {
320        #impl_block
321
322        #[repr(C)]
323        #[derive(
324            Clone,
325            Copy,
326            Debug,
327            Default,
328            crate::_private::bytemuck::Zeroable,
329            crate::_private::bytemuck::Pod,
330        )]
331        #(#struct_attrs)*
332        #vis struct #name {
333            #stripped_fields
334        }
335
336        impl #name {
337            pub const SIZE: usize = ::std::mem::size_of::<Self>();
338            #(#off_consts)*
339
340            /// Append human-readable field name/value pairs to the output string.
341            pub fn #dump_fields_ident(&self, out: &mut ::std::string::String) {
342                use ::std::fmt::Write as _;
343                #(#dump_lines)*
344            }
345        }
346
347        const _: () = {
348            fn assert_field_is_pod<T: crate::_private::bytemuck::Pod>() {}
349            #(let _ = assert_field_is_pod::<#field_types>;)*
350
351            ::wowlab_buffer_contract::assert_repr_c_layout(
352                &[#(#name::#offset_idents),*],
353                #name::SIZE,
354                &[#(#field_sizes_aligns),*],
355            );
356        };
357
358        #(#submissions)*
359    })
360}
361
362// #t(fn: rust_alloc_in_loop) generated descriptor text is allocated once per declared expression.
363fn build_submissions(
364    name: &Ident,
365    slot: &SlotAttr,
366    expr_attrs: &[ExprAttr],
367) -> Vec<proc_macro2::TokenStream> {
368    let mut items = Vec::with_capacity(expr_attrs.len() + 1);
369    let kind_ident = &slot.kind_ident;
370    let slot_kind_tokens = quote! { ::wowlab_buffer_contract::SlotKind::#kind_ident };
371    let key_domain_tokens = if let Some(kd) = &slot.key_domain {
372        quote! { Some(#kd) }
373    } else {
374        quote! { None }
375    };
376
377    let domain_str = &slot.domain;
378
379    items.push(quote! {
380        crate::_private::inventory::submit! {
381            ::wowlab_buffer_contract::SlotDescriptor {
382                name: #domain_str,
383                size: #name::SIZE,
384                kind: #slot_kind_tokens,
385            }
386        }
387    });
388
389    for expr in expr_attrs {
390        let domain = &slot.domain;
391        let expr_name = &expr.name;
392        let field_type_ident = &expr.field_type_ident;
393        let eval_kind_ident = &expr.eval_kind_ident;
394        let field_type_tokens = quote! { ::wowlab_buffer_contract::FieldType::#field_type_ident };
395        let eval_kind_tokens = quote! { ::wowlab_buffer_contract::EvalKind::#eval_kind_ident };
396        let description = format!("{}.{}", domain.value(), expr_name.value());
397
398        let off_ident = offset_const_name(&expr.field_ident);
399        let field_offset_tokens = quote! { #name::#off_ident };
400
401        items.push(quote! {
402            crate::_private::inventory::submit! {
403                ::wowlab_buffer_contract::FieldDescriptor {
404                    domain: #domain,
405                    name: #expr_name,
406                    field_type: #field_type_tokens,
407                    eval_kind: #eval_kind_tokens,
408                    field_offset: #field_offset_tokens,
409                    slot_size: #name::SIZE,
410                    slot_kind: #slot_kind_tokens,
411                    description: #description,
412                    key_domain: #key_domain_tokens,
413                }
414            }
415        });
416    }
417
418    items
419}
420
421// #t(fn: rust_alloc_in_loop) proc-macro expansion materializes one diagnostic label per field.
422fn build_dump_lines(
423    struct_name: &Ident,
424    fields: &Punctuated<Field, Comma>,
425) -> syn::Result<Vec<proc_macro2::TokenStream>> {
426    let mut lines = Vec::with_capacity(fields.len());
427
428    for field in fields {
429        let field_name = field.ident.as_ref().ok_or_else(|| {
430            syn::Error::new_spanned(
431                field,
432                format!("build_dump_lines({struct_name}): all fields must be named"),
433            )
434        })?;
435        let name_str = field_name.to_string();
436
437        if name_str.starts_with('_') {
438            continue;
439        }
440
441        let line = if is_f64(&field.ty) {
442            quote! {
443                let _ = writeln!(out, "  {:<24} {:.4}", #name_str, self.#field_name);
444            }
445        } else {
446            quote! {
447                let _ = writeln!(out, "  {:<24} {}", #name_str, self.#field_name);
448            }
449        };
450
451        lines.push(line);
452    }
453
454    Ok(lines)
455}
456
457struct FieldItems {
458    offset_consts: Vec<proc_macro2::TokenStream>,
459    offset_idents: Vec<Ident>,
460    sizes_and_alignments: Vec<proc_macro2::TokenStream>,
461    field_types: Vec<proc_macro2::TokenStream>,
462}
463
464// #t(fn: rust_alloc_in_loop) proc-macro expansion materializes spanned diagnostics for invalid fields.
465fn build_field_items(
466    struct_name: &Ident,
467    fields: &Punctuated<Field, Comma>,
468) -> syn::Result<FieldItems> {
469    let cap = fields.len();
470    let mut off_consts = Vec::with_capacity(cap);
471    let mut offset_idents = Vec::with_capacity(cap);
472    let mut field_sizes_aligns = Vec::with_capacity(cap);
473    let mut field_types = Vec::with_capacity(cap);
474
475    for field in fields {
476        let field_name = field.ident.as_ref().ok_or_else(|| {
477            syn::Error::new_spanned(
478                field,
479                format!("define_slot!({struct_name}): all fields must be named"),
480            )
481        })?;
482        let field_ty = &field.ty;
483        let field_vis = &field.vis;
484
485        let upper_name = offset_const_name(field_name);
486
487        let const_vis = match field_vis {
488            Visibility::Public(_) => quote! { pub },
489            _ => quote! {},
490        };
491
492        off_consts.push(quote! {
493            #const_vis const #upper_name: usize = ::std::mem::offset_of!(Self, #field_name);
494        });
495
496        offset_idents.push(upper_name);
497
498        field_sizes_aligns.push(quote! {
499            (::std::mem::size_of::<#field_ty>(), ::std::mem::align_of::<#field_ty>())
500        });
501        field_types.push(quote! { #field_ty });
502    }
503
504    Ok(FieldItems {
505        offset_consts: off_consts,
506        offset_idents,
507        sizes_and_alignments: field_sizes_aligns,
508        field_types,
509    })
510}
511
512#[cfg(test)]
513mod tests;