Skip to main content

khora_macros/
lib.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! This crate provides procedural macros for the Khora Engine.
16
17use proc_macro::TokenStream;
18use quote::{format_ident, quote};
19use syn::{parse_macro_input, Data, DeriveInput, Fields};
20
21/// A derive macro that implements the `khora_data::ecs::Component` trait
22/// and generates a serializable mirror struct with `From` conversions.
23///
24/// For a component like:
25/// ```ignore
26/// #[derive(Component)]
27/// pub struct Camera {
28///     pub projection: ProjectionType,
29///     pub aspect_ratio: f32,
30/// }
31/// ```
32///
33/// This macro generates:
34/// - `impl Component for Camera`
35/// - `pub struct SerializableCamera { ... }` with `Encode, Decode`
36/// - `impl From<Camera> for SerializableCamera`
37/// - `impl From<SerializableCamera> for Camera`
38///
39/// Use `#[component(skip)]` on fields that should not be serialized
40/// (e.g., GPU handles). Those fields are filled with `Default::default()`
41/// when deserializing back.
42#[proc_macro_derive(Component, attributes(component))]
43pub fn derive_component(input: TokenStream) -> TokenStream {
44    let input = parse_macro_input!(input as DeriveInput);
45    let name = &input.ident;
46    let vis = &input.vis;
47    let serializable_name = format_ident!("Serializable{}", name);
48    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
49
50    // Generate Component impl
51    let component_impl = quote! {
52        impl #impl_generics crate::ecs::component::Component for #name #ty_generics #where_clause {}
53    };
54
55    // Parse struct fields
56    let fields = match &input.data {
57        Data::Struct(data) => &data.fields,
58        _ => {
59            return TokenStream::from(quote! {
60                #component_impl
61                compile_error!("Component derive only supports structs");
62            });
63        }
64    };
65
66    // ── Field-SoA layout opt-in: `#[component(layout = "soa")]` ──────────
67    // A component can declare a field-split (Structure-of-Arrays) physical
68    // column — the AGDF compute-friendly layout. v1 supports structs whose
69    // fields are all `f32`. When opted in, the generated `Component` impl routes
70    // storage through a `FieldSoaColumn<Self>` and a `SoaLayout` impl describes
71    // the field↔lane mapping. Otherwise `component_impl` stays the default
72    // (empty marker) and no `SoaLayout` is emitted.
73    let layout_soa = input.attrs.iter().any(|attr| {
74        if !attr.path().is_ident("component") {
75            return false;
76        }
77        let mut soa = false;
78        let _ = attr.parse_nested_meta(|meta| {
79            if meta.path.is_ident("layout") {
80                if let Ok(value) = meta.value() {
81                    if let Ok(s) = value.parse::<syn::LitStr>() {
82                        soa = s.value() == "soa";
83                    }
84                }
85            }
86            Ok(())
87        });
88        soa
89    });
90
91    let (component_impl, soa_layout_impl) = if layout_soa {
92        let named = match fields {
93            Fields::Named(n) => n,
94            _ => {
95                return TokenStream::from(quote! {
96                    #component_impl
97                    compile_error!("#[component(layout = \"soa\")] requires a struct with named fields");
98                });
99            }
100        };
101        let mut field_idents = Vec::new();
102        for f in &named.named {
103            let is_f32 = matches!(&f.ty, syn::Type::Path(p) if p.path.is_ident("f32"));
104            if !is_f32 {
105                return TokenStream::from(quote! {
106                    #component_impl
107                    compile_error!("#[component(layout = \"soa\")] (v1) supports only `f32` fields");
108                });
109            }
110            field_idents.push(f.ident.clone().unwrap());
111        }
112        let field_count = field_idents.len();
113        let field_names: Vec<String> = field_idents.iter().map(|i| i.to_string()).collect();
114        let field_idx: Vec<usize> = (0..field_count).collect();
115
116        let soa_layout = quote! {
117            impl #impl_generics crate::ecs::soa::SoaLayout for #name #ty_generics #where_clause {
118                const FIELD_COUNT: usize = #field_count;
119                const FIELD_NAMES: &'static [&'static str] = &[#(#field_names),*];
120                fn scatter_push(&self, fields: &mut [Vec<f32>]) {
121                    #(fields[#field_idx].push(self.#field_idents);)*
122                }
123                fn scatter_set(&self, fields: &mut [Vec<f32>], row: usize) {
124                    #(fields[#field_idx][row] = self.#field_idents;)*
125                }
126                fn gather(fields: &[Vec<f32>], row: usize) -> Self {
127                    Self { #(#field_idents: fields[#field_idx][row]),* }
128                }
129            }
130        };
131        let comp_impl = quote! {
132            impl #impl_generics crate::ecs::component::Component for #name #ty_generics #where_clause {
133                fn make_column() -> Box<dyn crate::ecs::page::AnyVec> {
134                    Box::new(crate::ecs::soa::FieldSoaColumn::<#name>::new())
135                }
136                fn push_into_column(self, column: &mut dyn crate::ecs::page::AnyVec) {
137                    column
138                        .as_any_mut()
139                        .downcast_mut::<crate::ecs::soa::FieldSoaColumn<#name>>()
140                        .expect("SoA column type mismatch")
141                        .push(self);
142                }
143                fn copy_row_between(
144                    src: &dyn crate::ecs::page::AnyVec,
145                    src_row: usize,
146                    dst: &mut dyn crate::ecs::page::AnyVec,
147                ) {
148                    let value = src
149                        .as_any()
150                        .downcast_ref::<crate::ecs::soa::FieldSoaColumn<#name>>()
151                        .expect("SoA column type mismatch")
152                        .get(src_row);
153                    dst.as_any_mut()
154                        .downcast_mut::<crate::ecs::soa::FieldSoaColumn<#name>>()
155                        .expect("SoA column type mismatch")
156                        .push(value);
157                }
158                fn clone_from_column(column: &dyn crate::ecs::page::AnyVec, row: usize) -> Self {
159                    column
160                        .as_any()
161                        .downcast_ref::<crate::ecs::soa::FieldSoaColumn<#name>>()
162                        .expect("SoA column type mismatch")
163                        .get(row)
164                }
165                fn set_in_column(self, column: &mut dyn crate::ecs::page::AnyVec, row: usize) {
166                    column
167                        .as_any_mut()
168                        .downcast_mut::<crate::ecs::soa::FieldSoaColumn<#name>>()
169                        .expect("SoA column type mismatch")
170                        .set(row, self);
171                }
172            }
173        };
174        (comp_impl, soa_layout)
175    } else {
176        (component_impl, quote! {})
177    };
178
179    // Check for #[component(no_serializable)] attribute.
180    //
181    // The `else if` branch consumes unknown `key = value` metas (`domain`,
182    // `provenance`, `layout`) so the walk reaches this flag whatever the order
183    // the keys were written in — without it, `parse_nested_meta` aborts on the
184    // first unconsumed value and the discarded error makes the miss silent.
185    let no_serializable = input.attrs.iter().any(|attr| {
186        if !attr.path().is_ident("component") {
187            return false;
188        }
189        let mut no = false;
190        let _ = attr.parse_nested_meta(|meta| {
191            if meta.path.is_ident("no_serializable") {
192                no = true;
193            } else if meta.input.peek(syn::Token![=]) {
194                let _ = meta.value()?.parse::<syn::Expr>()?;
195            }
196            Ok(())
197        });
198        no
199    });
200
201    // Parse the type-level `#[component(...)]` keys that carry a value:
202    //
203    // * `domain = <SemanticDomain variant>` — the component self-registers its
204    //   domain into `World` via `inventory`, so the domain lives on the type
205    //   instead of a hand-maintained list in `World::new`.
206    // * `provenance = <ComponentProvenance variant>` — who writes the component.
207    //   Absent means `Authored`: a component is the author's data unless it says
208    //   otherwise, so engine-written types opt out explicitly rather than every
209    //   authored type having to opt in.
210    //
211    // Both keys are read in ONE pass. `parse_nested_meta` requires every meta it
212    // walks to be fully consumed, so a closure that recognises only one key
213    // aborts on the other's `= value` — and because the result is discarded, the
214    // failure is silent and the second key is simply never seen. Hence the
215    // trailing branch that consumes unknown `key = value` pairs (`layout = ...`)
216    // so parsing continues past them.
217    let (domain_ident, provenance_ident): (Option<syn::Ident>, syn::Ident) = {
218        let mut domain = None;
219        let mut provenance = None;
220        for attr in &input.attrs {
221            if !attr.path().is_ident("component") {
222                continue;
223            }
224            let _ = attr.parse_nested_meta(|meta| {
225                if meta.path.is_ident("domain") {
226                    domain = Some(meta.value()?.parse::<syn::Ident>()?);
227                } else if meta.path.is_ident("provenance") {
228                    provenance = Some(meta.value()?.parse::<syn::Ident>()?);
229                } else if meta.input.peek(syn::Token![=]) {
230                    let _ = meta.value()?.parse::<syn::Expr>()?;
231                }
232                Ok(())
233            });
234        }
235        let provenance = provenance
236            .unwrap_or_else(|| syn::Ident::new("Authored", proc_macro::Span::call_site().into()));
237        (domain, provenance)
238    };
239
240    let domain_registration = match &domain_ident {
241        Some(domain) => quote! {
242            inventory::submit! {
243                crate::ecs::ComponentDomainRegistration {
244                    register: |world: &mut crate::ecs::World| {
245                        world.register_component::<#name>(crate::ecs::SemanticDomain::#domain);
246                    }
247                }
248            }
249        },
250        None => quote! {},
251    };
252
253    if no_serializable {
254        return TokenStream::from(quote! {
255            #component_impl
256            #domain_registration
257            #soa_layout_impl
258        });
259    }
260
261    // Separate included and skipped fields
262    let mut included_fields = Vec::new();
263    let mut skipped_fields = Vec::new();
264
265    for field in fields.iter() {
266        let is_skip = field.attrs.iter().any(|attr| {
267            if !attr.path().is_ident("component") {
268                return false;
269            }
270            let mut skip = false;
271            let _ = attr.parse_nested_meta(|meta| {
272                if meta.path.is_ident("skip") {
273                    skip = true;
274                }
275                Ok(())
276            });
277            skip
278        });
279
280        if is_skip {
281            skipped_fields.push(field);
282        } else {
283            included_fields.push(field);
284        }
285    }
286
287    // Generate Serializable struct fields (only included fields)
288    let serializable_field_defs: Vec<_> = included_fields
289        .iter()
290        .map(|f| {
291            let fvis = &f.vis;
292            let fname = &f.ident;
293            let ftype = &f.ty;
294            if let Some(fname) = fname {
295                quote! { #fvis #fname: #ftype }
296            } else {
297                quote! { #fvis #ftype }
298            }
299        })
300        .collect();
301
302    // Generate field assignments for From<Original> → Serializable
303    let to_serializable_assigns: Vec<_> = included_fields
304        .iter()
305        .map(|f| {
306            let fname = &f.ident;
307            if fname.is_some() {
308                quote! { #fname: value.#fname }
309            } else {
310                quote! { value.#fname }
311            }
312        })
313        .collect();
314
315    // Generate field assignments for From<Serializable> → Original
316    // Included fields come from the serializable, skipped fields use Default
317    let from_serializable_included: Vec<_> = included_fields
318        .iter()
319        .map(|f| {
320            let fname = &f.ident;
321            if fname.is_some() {
322                quote! { #fname: serializable.#fname }
323            } else {
324                quote! { serializable.#fname }
325            }
326        })
327        .collect();
328
329    let from_serializable_skipped: Vec<_> = skipped_fields
330        .iter()
331        .map(|f| {
332            let fname = &f.ident;
333            if fname.is_some() {
334                quote! { #fname: Default::default() }
335            } else {
336                quote! { Default::default() }
337            }
338        })
339        .collect();
340
341    let all_from_fields: Vec<_> = from_serializable_included
342        .into_iter()
343        .chain(from_serializable_skipped)
344        .collect();
345
346    // Determine struct kind for Serializable.
347    //
348    // The `Serializable<Name>` mirror is a generated implementation detail
349    // used by the scene serialization pipeline and the editor inspector.
350    // It is `pub` because callers of `#[derive(Component)]` can be public,
351    // but it is not part of the documented public API surface. We tag it
352    // with `#[allow(missing_docs)]` so that crates with `#![warn(missing_docs)]`
353    // do not require users to document every mirror field by hand.
354    let serializable_struct = match fields {
355        Fields::Named(_) if serializable_field_defs.is_empty() => {
356            // All fields skipped → unit struct
357            quote! {
358                #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
359                #[allow(missing_docs)]
360                #[doc(hidden)]
361                #vis struct #serializable_name;
362            }
363        }
364        Fields::Named(_) => {
365            quote! {
366                #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
367                #[allow(missing_docs)]
368                #[doc(hidden)]
369                #vis struct #serializable_name {
370                    #(#serializable_field_defs),*
371                }
372            }
373        }
374        Fields::Unnamed(_) => {
375            let field_types: Vec<_> = included_fields.iter().map(|f| &f.ty).collect();
376            let field_vis: Vec<_> = included_fields.iter().map(|f| &f.vis).collect();
377            quote! {
378                #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
379                #[allow(missing_docs)]
380                #[doc(hidden)]
381                #vis struct #serializable_name(#(#field_vis #field_types),*);
382            }
383        }
384        Fields::Unit => {
385            quote! {
386                #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bincode::Encode, bincode::Decode)]
387                #[allow(missing_docs)]
388                #[doc(hidden)]
389                #vis struct #serializable_name;
390            }
391        }
392    };
393
394    // Generate From impls
395    let from_original_to_serializable = if matches!(fields, Fields::Named(_))
396        && !serializable_field_defs.is_empty()
397    {
398        quote! {
399            impl From<#name> for #serializable_name {
400                fn from(value: #name) -> Self {
401                    Self {
402                        #(#to_serializable_assigns),*
403                    }
404                }
405            }
406        }
407    } else if matches!(fields, Fields::Unnamed(_)) {
408        let indices: Vec<syn::Index> = (0..included_fields.len()).map(syn::Index::from).collect();
409        quote! {
410            impl From<#name> for #serializable_name {
411                fn from(value: #name) -> Self {
412                    Self(#(value.#indices),*)
413                }
414            }
415        }
416    } else {
417        quote! {}
418    };
419
420    let from_serializable_to_original = if matches!(fields, Fields::Named(_))
421        && !serializable_field_defs.is_empty()
422    {
423        quote! {
424            impl From<#serializable_name> for #name {
425                fn from(serializable: #serializable_name) -> Self {
426                    Self {
427                        #(#all_from_fields),*
428                    }
429                }
430            }
431        }
432    } else if matches!(fields, Fields::Unnamed(_)) {
433        let indices: Vec<syn::Index> = (0..included_fields.len()).map(syn::Index::from).collect();
434        quote! {
435            impl From<#serializable_name> for #name {
436                fn from(serializable: #serializable_name) -> Self {
437                    Self(#(serializable.#indices),*)
438                }
439            }
440        }
441    } else {
442        quote! {}
443    };
444
445    let expanded = quote! {
446        #component_impl
447        #domain_registration
448        #soa_layout_impl
449        #serializable_struct
450        #from_original_to_serializable
451        #from_serializable_to_original
452
453        // Auto-register this component for scene serialization AND for the
454        // editor's generic inspector (the latter via the `*_json` pair,
455        // which goes through serde on the same Serializable mirror as
456        // bincode does — so anything tagged `#[component(skip)]` is
457        // omitted from both paths automatically).
458        inventory::submit! {
459            crate::scene::ComponentRegistration {
460                type_id: std::any::TypeId::of::<#name>(),
461                type_name: stringify!(#name),
462                provenance: crate::ecs::ComponentProvenance::#provenance_ident,
463                serialize_recipe: |world, entity| {
464                    // By value so it works for any column layout (AoS or field-SoA).
465                    world.clone_component::<#name>(entity).map(|c| {
466                        bincode::encode_to_vec(&<#serializable_name>::from(c), bincode::config::standard())
467                            .unwrap_or_default()
468                    })
469                },
470                deserialize_recipe: |world, entity, data| {
471                    let (s, _): (#serializable_name, _) = bincode::decode_from_slice_with_context(
472                        data, bincode::config::standard(), ()
473                    ).map_err(|e| e.to_string())?;
474                    world.add_component(entity, <#name>::from(s)).ok();
475                    Ok(())
476                },
477                create_default: |world, entity| {
478                    world.add_component(entity, <#name>::default()).ok();
479                    Ok(())
480                },
481                to_json: |world, entity| {
482                    world.clone_component::<#name>(entity).and_then(|c| {
483                        serde_json::to_value(<#serializable_name>::from(c)).ok()
484                    })
485                },
486                from_json: |world, entity, value| {
487                    let s: #serializable_name = serde_json::from_value(value.clone())
488                        .map_err(|e| e.to_string())?;
489                    let new_value = <#name>::from(s);
490                    // Layout-agnostic write: update in place if present, else add.
491                    if !world.set_component(entity, new_value.clone()) {
492                        world.add_component(entity, new_value)
493                            .map_err(|e| format!("{:?}", e))?;
494                    }
495                    Ok(())
496                },
497                remove: |world, entity| {
498                    // Surgical single-component remove — preserves every
499                    // other component on the entity (any domain).
500                    match world.remove_component::<#name>(entity) {
501                        Ok(_) => Ok(()),
502                        Err(e) => Err(format!("{:?}", e)),
503                    }
504                },
505            }
506        }
507    };
508
509    TokenStream::from(expanded)
510}