ommx_derive/lib.rs
1//! Derive macros for the `ommx` crate.
2//!
3//! This crate exists solely as an implementation detail of [`ommx`]: it
4//! is published to crates.io because `ommx` depends on it, but it has no
5//! stable API of its own and **no public surface for external use**.
6//!
7//! The `ommx` crate gates the `LogicalMemoryProfile` trait and the
8//! re-exported derive behind a `pub(crate)` module, so downstream users
9//! cannot reach them through the public API and cannot meaningfully
10//! derive on their own types. The trait and the re-export are declared
11//! `pub` *inside* that module to satisfy the `private_bounds` lint when
12//! the trait appears in the bound of a `pub` type within the crate
13//! (e.g. `ConstraintMetadataStore<ID: ... + LogicalMemoryProfile>`).
14//! External consumers should use
15//! [`ommx::Instance::logical_memory_profile`] and
16//! [`ommx::MemoryProfile`] instead.
17//!
18//! [`ommx`]: https://docs.rs/ommx
19//! [`ommx::Instance::logical_memory_profile`]: https://docs.rs/ommx/latest/ommx/struct.Instance.html#method.logical_memory_profile
20//! [`ommx::MemoryProfile`]: https://docs.rs/ommx/latest/ommx/struct.MemoryProfile.html
21//!
22//! # `#[derive(LogicalMemoryProfile)]`
23//!
24//! Generates a `LogicalMemoryProfile` impl that delegates to each field
25//! of a named-field struct. Each field is emitted under the path frame
26//! `"TypeName.field_name"`. The `ommx` crate uses this derive at every
27//! struct definition that participates in memory profiling, so that
28//! adding or removing a field automatically adjusts the profile.
29//!
30//! ## Supported
31//!
32//! - Structs with named fields.
33//! - All fields must implement `LogicalMemoryProfile`. Primitives,
34//! `String`, `Option<T>`, `Vec<T>`, `BTreeMap`, `HashMap`,
35//! `FnvHashMap`, and `BTreeSet` all have blanket impls in
36//! `ommx::logical_memory::collections`.
37//! - Generic structs: type parameters are propagated through, but
38//! **no `LogicalMemoryProfile` bound is added automatically**. The
39//! struct must declare its own `where T: LogicalMemoryProfile`
40//! clause. This matches `serde`'s historical `#[serde(bound = ...)]`
41//! philosophy — the derive does not guess.
42//!
43//! ## Not supported
44//!
45//! - Tuple structs and unit structs → emit a `compile_error!` directing
46//! the caller to a hand-written impl.
47//! - Enums → emit a `compile_error!`. For enums, hand-write a `match`
48//! (`Function` in the `ommx` crate is an example).
49//! - Field skipping → there is no `#[logical_memory(skip)]` attribute.
50//! If a field truly should not participate, hand-write the impl.
51//! - Custom frame names → the frame is always `"TypeName.field_name"`
52//! taken from the struct ident and field ident. For a renamed frame
53//! (e.g. when wrapping an external type), use the declarative
54//! `impl_logical_memory_profile! { path::to::Type as "Name" { ... } }`
55//! form instead.
56//!
57//! # Testing
58//!
59//! The proc-macro entry point delegates to
60//! `derive_logical_memory_profile_impl`, a pure
61//! `TokenStream2 -> TokenStream2` function. This is exercised by inline
62//! `insta` snapshot tests in this crate — the generated code is checked
63//! in as a snapshot so any drift is caught at review time.
64
65use proc_macro::TokenStream;
66use proc_macro2::TokenStream as TokenStream2;
67use quote::quote;
68use syn::{Data, DeriveInput, Fields};
69
70/// Derive `LogicalMemoryProfile` for a struct by delegating to each field.
71///
72/// Only structs with named fields are supported. Each field's profile is
73/// emitted under the path frame `"TypeName.field_name"`.
74#[proc_macro_derive(LogicalMemoryProfile)]
75pub fn derive_logical_memory_profile(input: TokenStream) -> TokenStream {
76 derive_logical_memory_profile_impl(input.into()).into()
77}
78
79/// Pure `TokenStream2` entry point for the derive.
80///
81/// Split out from the `#[proc_macro_derive]` wrapper so that unit tests
82/// can exercise the code-generation logic without the proc-macro runtime.
83fn derive_logical_memory_profile_impl(input: TokenStream2) -> TokenStream2 {
84 let input = match syn::parse2::<DeriveInput>(input) {
85 Ok(ast) => ast,
86 Err(err) => return err.to_compile_error(),
87 };
88 let name = &input.ident;
89 let name_str = name.to_string();
90
91 let fields = match &input.data {
92 Data::Struct(data) => match &data.fields {
93 Fields::Named(fields) => &fields.named,
94 _ => {
95 return syn::Error::new_spanned(
96 name,
97 "LogicalMemoryProfile derive only supports structs with named fields",
98 )
99 .to_compile_error();
100 }
101 },
102 _ => {
103 return syn::Error::new_spanned(
104 name,
105 "LogicalMemoryProfile derive only supports structs",
106 )
107 .to_compile_error();
108 }
109 };
110
111 let field_visits = fields.iter().map(|field| {
112 let field_name = field.ident.as_ref().expect("named field");
113 let frame = format!("{name_str}.{field_name}");
114 quote! {
115 ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
116 &self.#field_name,
117 path.with(#frame).as_mut(),
118 visitor,
119 );
120 }
121 });
122
123 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
124
125 quote! {
126 impl #impl_generics ::ommx::logical_memory::LogicalMemoryProfile
127 for #name #ty_generics #where_clause
128 {
129 fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
130 &self,
131 path: &mut ::ommx::logical_memory::Path,
132 visitor: &mut __V,
133 ) {
134 #( #field_visits )*
135 }
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 /// Render a derive-generated `TokenStream2` as a formatted Rust source
145 /// string, so `insta::assert_snapshot!` diffs are readable.
146 fn render(input: TokenStream2) -> String {
147 let tokens = derive_logical_memory_profile_impl(input);
148 let file: syn::File = syn::parse2(tokens).expect("derive output must parse as syn::File");
149 prettyplease::unparse(&file)
150 }
151
152 #[test]
153 fn snapshot_flat_struct() {
154 let input = quote! {
155 struct Foo {
156 a: u64,
157 b: String,
158 }
159 };
160 insta::assert_snapshot!(render(input), @r###"
161 impl ::ommx::logical_memory::LogicalMemoryProfile for Foo {
162 fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
163 &self,
164 path: &mut ::ommx::logical_memory::Path,
165 visitor: &mut __V,
166 ) {
167 ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
168 &self.a,
169 path.with("Foo.a").as_mut(),
170 visitor,
171 );
172 ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
173 &self.b,
174 path.with("Foo.b").as_mut(),
175 visitor,
176 );
177 }
178 }
179 "###);
180 }
181
182 #[test]
183 fn snapshot_single_field_struct() {
184 let input = quote! {
185 struct Wrapper {
186 inner: Inner,
187 }
188 };
189 insta::assert_snapshot!(render(input), @r###"
190 impl ::ommx::logical_memory::LogicalMemoryProfile for Wrapper {
191 fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
192 &self,
193 path: &mut ::ommx::logical_memory::Path,
194 visitor: &mut __V,
195 ) {
196 ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
197 &self.inner,
198 path.with("Wrapper.inner").as_mut(),
199 visitor,
200 );
201 }
202 }
203 "###);
204 }
205
206 #[test]
207 fn snapshot_empty_struct() {
208 // Unit-like structs with empty named-field bodies are legal; the
209 // derive should emit an empty `visit_logical_memory` body.
210 let input = quote! {
211 struct Empty {}
212 };
213 insta::assert_snapshot!(render(input), @r###"
214 impl ::ommx::logical_memory::LogicalMemoryProfile for Empty {
215 fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
216 &self,
217 path: &mut ::ommx::logical_memory::Path,
218 visitor: &mut __V,
219 ) {}
220 }
221 "###);
222 }
223
224 #[test]
225 fn snapshot_generic_struct() {
226 // Generic parameters are propagated without automatic trait-bound
227 // injection; callers must ensure `T: LogicalMemoryProfile` themselves
228 // (e.g. via a `where` clause on the struct definition).
229 let input = quote! {
230 struct Generic<T> where T: ::ommx::logical_memory::LogicalMemoryProfile {
231 value: T,
232 count: u64,
233 }
234 };
235 insta::assert_snapshot!(render(input), @r###"
236 impl<T> ::ommx::logical_memory::LogicalMemoryProfile for Generic<T>
237 where
238 T: ::ommx::logical_memory::LogicalMemoryProfile,
239 {
240 fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
241 &self,
242 path: &mut ::ommx::logical_memory::Path,
243 visitor: &mut __V,
244 ) {
245 ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
246 &self.value,
247 path.with("Generic.value").as_mut(),
248 visitor,
249 );
250 ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
251 &self.count,
252 path.with("Generic.count").as_mut(),
253 visitor,
254 );
255 }
256 }
257 "###);
258 }
259
260 #[test]
261 fn snapshot_rejects_enum() {
262 // Error output is also snapshot-tested to lock in the diagnostic
263 // message surface. The generated compile_error! invocation is the
264 // contract for non-struct inputs.
265 let input = quote! {
266 enum NotSupported { A, B }
267 };
268 insta::assert_snapshot!(render(input), @r###"
269 ::core::compile_error! {
270 "LogicalMemoryProfile derive only supports structs"
271 }
272 "###);
273 }
274
275 #[test]
276 fn snapshot_rejects_tuple_struct() {
277 let input = quote! {
278 struct Tuple(u64, String);
279 };
280 insta::assert_snapshot!(render(input), @r###"
281 ::core::compile_error! {
282 "LogicalMemoryProfile derive only supports structs with named fields"
283 }
284 "###);
285 }
286}