wowlab_engine_macros_impl/
codegen.rs1use 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
36fn 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
206pub(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 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
362fn 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
421fn 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
464fn 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;