Skip to main content

wowlab_engine_macros_impl/
lower.rs

1use quote::quote;
2use syn::{Data, DeriveInput, Fields, Ident, Path, PathArguments};
3
4use crate::lower_parse::{LowerFieldMode, parse_lower_container, parse_lower_field_mode};
5
6struct FieldSpec {
7    ident: Ident,
8    mode: LowerFieldMode,
9}
10
11pub(crate) fn expand_lower_data(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
12    let Data::Struct(data_struct) = &input.data else {
13        return Err(syn::Error::new_spanned(
14            &input.ident,
15            "#[derive(LowerData)] can only be applied to structs",
16        ));
17    };
18
19    let Fields::Named(named) = &data_struct.fields else {
20        return Err(syn::Error::new_spanned(
21            &input.ident,
22            "#[derive(LowerData)] requires a struct with named fields",
23        ));
24    };
25
26    let def_ident = &input.ident;
27    let container = parse_lower_container(input)?;
28    let field_specs: Vec<FieldSpec> = named
29        .named
30        .iter()
31        .map(|field| {
32            let ident = field
33                .ident
34                .clone()
35                .ok_or_else(|| syn::Error::new_spanned(field, "field must be named"))?;
36            let mode = parse_lower_field_mode(field)?;
37
38            Ok(FieldSpec { ident, mode })
39        })
40        .collect::<syn::Result<_>>()?;
41
42    reject_duplicate_data_initializers(&field_specs, &container.extra)?;
43
44    let data_ty = &container.data;
45    let data_expr = expression_path(data_ty);
46    let ctx_ty = &container.ctx;
47    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
48    let data_inits = data_initializers(&field_specs, &container.extra);
49
50    let into_data = quote! {
51        impl #impl_generics #def_ident #type_generics #where_clause {
52            /// Lower this builder def into its `Copy` engine-side data struct.
53            ///
54            /// Generated by `#[derive(LowerData)]`.
55            pub(crate) fn into_data(&self, ctx: &mut #ctx_ty) -> #data_ty {
56                #data_expr {
57                    #(#data_inits,)*
58                }
59            }
60        }
61    };
62    let into_info = container
63        .info
64        .as_ref()
65        .map_or_else(proc_macro2::TokenStream::new, |info| {
66            info_impl(input, data_ty, info)
67        });
68    let finalize = container.finalize.as_ref().map_or_else(
69        || Ok(proc_macro2::TokenStream::new()),
70        |finalize| finalize_impl(input, &field_specs, finalize),
71    )?;
72
73    Ok(quote! {
74        #into_data
75        #into_info
76        #finalize
77    })
78}
79
80fn data_initializers(
81    fields: &[FieldSpec],
82    extra: &[crate::lower_parse::LowerNamedFn],
83) -> Vec<proc_macro2::TokenStream> {
84    let field_initializers = fields.iter().filter_map(|spec| {
85        let ident = &spec.ident;
86
87        match &spec.mode {
88            LowerFieldMode::Copy => Some(quote!(#ident: self.#ident)),
89            LowerFieldMode::Skip => None,
90            LowerFieldMode::NameIdx(resolve) => Some(quote!(name_idx: #resolve(&self.#ident, ctx))),
91            LowerFieldMode::Convert(func) => Some(quote!(#ident: #func(self, ctx))),
92        }
93    });
94    let extra_initializers = extra.iter().map(|entry| {
95        let field = &entry.field;
96        let func = &entry.func;
97
98        quote!(#field: #func(self, ctx))
99    });
100
101    field_initializers.chain(extra_initializers).collect()
102}
103
104fn info_impl(
105    input: &DeriveInput,
106    data_ty: &Path,
107    info: &crate::lower_parse::LowerInfoSpec,
108) -> proc_macro2::TokenStream {
109    let info_ty = &info.ty;
110    let info_expr = expression_path(info_ty);
111    let info_ctx = &info.ctx;
112    let info_inits = info.fields.iter().map(|named_field| {
113        let field = &named_field.field;
114
115        named_field.func.as_ref().map_or_else(
116            || quote!(#field: self.#field),
117            |func| quote!(#field: #func(self, ctx)),
118        )
119    });
120    let (impl_generics, _, where_clause) = input.generics.split_for_impl();
121
122    quote! {
123        impl #impl_generics #data_ty #where_clause {
124            /// Lower this engine-side data struct into its introspection-facing info struct.
125            ///
126            /// Generated by `#[derive(LowerData)]` on the corresponding `*Def`.
127            pub(crate) fn into_info(&self, ctx: &#info_ctx) -> #info_ty {
128                #info_expr {
129                    #(#info_inits,)*
130                }
131            }
132        }
133    }
134}
135
136fn finalize_impl(
137    input: &DeriveInput,
138    fields: &[FieldSpec],
139    finalize: &crate::lower_parse::LowerFinalizeSpec,
140) -> syn::Result<proc_macro2::TokenStream> {
141    validate_finalize_fields(fields, finalize)?;
142
143    let source = &finalize.source;
144    let method = &finalize.method;
145    let args = finalize.args.iter().map(|arg| {
146        let field = &arg.field;
147        let ty = &arg.ty;
148
149        quote!(#field: #ty)
150    });
151    let field_inits = fields.iter().map(|spec| {
152        let field = &spec.ident;
153
154        if finalize.args.iter().any(|arg| arg.field == *field) {
155            quote!(#field)
156        } else if let Some(rename) = finalize
157            .renames
158            .iter()
159            .find(|rename| rename.field == *field)
160        {
161            let source_field = &rename.source;
162
163            quote!(#field: source.#source_field)
164        } else {
165            quote!(#field: source.#field)
166        }
167    });
168    let def_ident = &input.ident;
169    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
170
171    Ok(quote! {
172        impl #impl_generics #def_ident #type_generics #where_clause {
173            /// Finalize an owned builder draft into this definition.
174            ///
175            /// Generated by `#[derive(LowerData)]`.
176            pub(crate) fn #method(source: #source, #(#args,)*) -> Self {
177                Self {
178                    #(#field_inits,)*
179                }
180            }
181        }
182    })
183}
184
185fn expression_path(path: &Path) -> Path {
186    let mut path = path.clone();
187
188    if let Some(last) = path.segments.last_mut()
189        && let PathArguments::AngleBracketed(arguments) = &mut last.arguments
190    {
191        arguments.colon2_token = Some(syn::token::PathSep::default());
192    }
193
194    path
195}
196
197// #t(fn: rust_alloc_in_loop) diagnostics allocate only while rejecting invalid macro input.
198fn reject_duplicate_data_initializers(
199    fields: &[FieldSpec],
200    extra: &[crate::lower_parse::LowerNamedFn],
201) -> syn::Result<()> {
202    let mut saw_name_idx = false;
203
204    for spec in fields {
205        if matches!(&spec.mode, LowerFieldMode::NameIdx(_)) {
206            if saw_name_idx {
207                return Err(syn::Error::new_spanned(
208                    &spec.ident,
209                    "multiple `name_idx` modes would generate duplicate `name_idx` initializers",
210                ));
211            }
212
213            saw_name_idx = true;
214        }
215    }
216
217    for entry in extra {
218        let duplicates_field = fields.iter().any(|spec| match &spec.mode {
219            LowerFieldMode::Copy | LowerFieldMode::Convert(_) => spec.ident == entry.field,
220            LowerFieldMode::NameIdx(_) => entry.field == "name_idx",
221            LowerFieldMode::Skip => false,
222        });
223
224        if duplicates_field {
225            return Err(syn::Error::new_spanned(
226                &entry.field,
227                format!(
228                    "`extra` initializer `{}` duplicates a field initializer",
229                    entry.field
230                ),
231            ));
232        }
233    }
234
235    Ok(())
236}
237
238fn validate_finalize_fields(
239    fields: &[FieldSpec],
240    finalize: &crate::lower_parse::LowerFinalizeSpec,
241) -> syn::Result<()> {
242    for (index, arg) in finalize.args.iter().enumerate() {
243        if !fields.iter().any(|field| field.ident == arg.field) {
244            return Err(syn::Error::new_spanned(
245                &arg.field,
246                "finalize argument is not a target field",
247            ));
248        }
249
250        if finalize
251            .args
252            .iter()
253            .take(index)
254            .any(|earlier| earlier.field == arg.field)
255        {
256            return Err(syn::Error::new_spanned(
257                &arg.field,
258                "duplicate finalize argument",
259            ));
260        }
261    }
262
263    for (index, rename) in finalize.renames.iter().enumerate() {
264        if !fields.iter().any(|field| field.ident == rename.field) {
265            return Err(syn::Error::new_spanned(
266                &rename.field,
267                "finalize rename is not a target field",
268            ));
269        }
270
271        if finalize
272            .renames
273            .iter()
274            .take(index)
275            .any(|earlier| earlier.field == rename.field)
276            || finalize.args.iter().any(|arg| arg.field == rename.field)
277        {
278            return Err(syn::Error::new_spanned(
279                &rename.field,
280                "duplicate finalize initializer",
281            ));
282        }
283    }
284
285    Ok(())
286}
287
288#[cfg(test)]
289mod tests {
290    use googletest::prelude::*;
291
292    use super::*;
293
294    #[gtest]
295    fn lower_data_expansion_copies_declared_fields() -> Result<()> {
296        let input: DeriveInput = syn::parse_quote! {
297            #[lower(data = "RecordData", ctx = "LowerContext")]
298            struct RecordDef {
299                #[lower(copy)]
300                value: i32,
301            }
302        };
303
304        let output = expand_lower_data(&input).or_fail()?;
305        let expected = quote! {
306            impl RecordDef {
307                /// Lower this builder def into its `Copy` engine-side data struct.
308                ///
309                /// Generated by `#[derive(LowerData)]`.
310                pub(crate) fn into_data(&self, ctx: &mut LowerContext) -> RecordData {
311                    RecordData {
312                        value: self.value,
313                    }
314                }
315            }
316        };
317
318        verify_that!(output.to_string(), eq(&expected.to_string()))
319    }
320
321    #[gtest]
322    fn lower_data_expansion_preserves_input_generics() -> Result<()> {
323        let input: DeriveInput = syn::parse_quote! {
324            #[lower(data = "RecordData<T>", ctx = "LowerContext")]
325            struct RecordDef<T>
326            where
327                T: Copy,
328            {
329                #[lower(copy)]
330                value: T,
331            }
332        };
333
334        let output = expand_lower_data(&input).or_fail()?;
335        let expected = quote! {
336            impl<T> RecordDef<T>
337            where
338                T: Copy,
339            {
340                /// Lower this builder def into its `Copy` engine-side data struct.
341                ///
342                /// Generated by `#[derive(LowerData)]`.
343                pub(crate) fn into_data(&self, ctx: &mut LowerContext) -> RecordData<T> {
344                    RecordData::<T> {
345                        value: self.value,
346                    }
347                }
348            }
349        };
350
351        verify_that!(output.to_string(), eq(&expected.to_string()))
352    }
353
354    #[gtest]
355    fn duplicate_extra_initializer_is_rejected_at_the_duplicate_field() -> Result<()> {
356        let input: DeriveInput = syn::parse_quote! {
357            #[lower(
358                data = "RecordData",
359                ctx = "LowerContext",
360                extra(value = make_value)
361            )]
362            struct RecordDef {
363                #[lower(copy)]
364                value: i32,
365            }
366        };
367
368        let error = expand_lower_data(&input).err().or_fail()?;
369
370        verify_that!(
371            error.to_string(),
372            eq("`extra` initializer `value` duplicates a field initializer")
373        )
374    }
375
376    #[gtest]
377    fn multiple_name_idx_modes_are_rejected_before_generating_duplicate_fields() -> Result<()> {
378        let input: DeriveInput = syn::parse_quote! {
379            #[lower(data = "RecordData", ctx = "LowerContext")]
380            struct RecordDef {
381                #[lower(name_idx = resolve_first)]
382                first_name: String,
383                #[lower(name_idx = resolve_second)]
384                second_name: String,
385            }
386        };
387
388        let error = expand_lower_data(&input).err().or_fail()?;
389
390        verify_that!(
391            error.to_string(),
392            eq("multiple `name_idx` modes would generate duplicate `name_idx` initializers")
393        )
394    }
395
396    #[gtest]
397    fn lower_data_generates_finalize_direction() -> Result<()> {
398        let input: DeriveInput = syn::parse_quote! {
399            #[lower(
400                data = "RecordData",
401                ctx = "LowerContext",
402                finalize = "RecordDraft",
403                finalize_args(id: u32),
404                finalize_rename(value = draft_value)
405            )]
406            struct RecordDef {
407                #[lower(copy)]
408                id: u32,
409                #[lower(copy)]
410                value: i32,
411                #[lower(copy)]
412                ordinary: bool,
413            }
414        };
415
416        let output = expand_lower_data(&input).or_fail()?.to_string();
417
418        verify_that!(output, contains_substring("fn from_draft"))?;
419        verify_that!(output, contains_substring("Self { id ,"))?;
420        verify_that!(output, contains_substring("value : source . draft_value"))?;
421
422        verify_that!(output, contains_substring("ordinary : source . ordinary"))
423    }
424}