whippyunits_proc_macros/
lib.rs

1#![feature(trait_alias)]
2#![allow(mixed_script_confusables)]
3
4use proc_macro::TokenStream;
5use syn::parse_macro_input;
6
7mod compute_unit_dimensions_macro;
8mod define_generic_dimension_macro;
9mod define_literals_macro;
10mod define_local_quantity_macro;
11mod define_unit_declarators_macro;
12mod generate_all_radian_erasures_macro;
13mod generate_default_declarators_macro;
14mod generate_literals_module_macro;
15mod generate_local_unit_literals_macro;
16mod local_unit_type_macro;
17mod pow_lookup_macro;
18mod quantity_macro;
19mod unit_macro;
20mod value_macro;
21
22mod utils {
23    pub mod culit;
24    pub mod dimension_suggestions;
25    pub mod lift_trace;
26    pub mod literal_macros;
27    pub mod scale_suggestions;
28    pub mod shared_utils;
29    pub mod unit_suggestions;
30}
31
32#[proc_macro]
33#[doc(hidden)]
34pub fn compute_unit_dimensions(input: TokenStream) -> TokenStream {
35    compute_unit_dimensions_macro::compute_unit_dimensions(input)
36}
37
38#[proc_macro]
39pub fn define_generic_dimension(input: TokenStream) -> TokenStream {
40    let input =
41        parse_macro_input!(input as define_generic_dimension_macro::DefineGenericDimensionInput);
42    input.expand().into()
43}
44
45/// Creates a concrete [Quantity] type from a unit expression.
46///
47/// This is particularly useful for constraining the result of potentially-type-ambiguous operations,
48/// such as multiplication of two quantities with different dimensions.  If you want to construct a
49/// quantity with a known value, use the `quantity!` macro instead.
50///
51/// ## Syntax
52///
53/// ```rust,ignore
54/// unit!(unit_expr);
55/// unit!(unit_expr, storage_type);
56/// ```
57///
58/// Where:
59/// - `unit_expr`: A "unit literal expression"
60///     - A "unit literal expression" is either:
61///         - An atomic unit (may include prefix):
62///             - `m`, `kg`, `s`, `A`, `K`, `mol`, `cd`, `rad`
63///         - An exponentiation of an atomic unit:
64///             - `m2`, `m^2`
65///         - A multiplication of two or more (possibly exponentiated) atomic units:
66///             - `kg.m2`, `kg * m2`
67///         - A division of two such product expressions:
68///             - `kg.m2/s2`, `kg * m2 / s^2`
69///             - There may be at most one division expression in a unit literal expression
70///             - All terms trailing the division symbol are considered to be in the denominator
71/// - `storage_type`: An optional storage type for the quantity. Defaults to `f64`.
72///
73/// ## Examples
74///
75/// ```rust
76/// # #[culit::culit(whippyunits::default_declarators::literals)]
77/// # fn main() {
78/// # use whippyunits::api::rescale;
79/// # use whippyunits::unit;
80/// // Constrain a multiplication to compile error if the units are wrong:
81/// let area = 5.0m * 5.0m; // ⚠️ Correct, but unchecked; will compile regardless of the units
82/// let area = 5.0m * 5.0s; // ❌ BUG: compiles fine, but is not an area
83/// let area: unit!(m^2) = 5.0m * 5.0m; // ✅ Correct, will compile only if the units are correct
84/// // let area: unit!(m^2) = 5.0m * 5.0s; // 🚫 Compile error, as expected
85///
86/// // Specify the target dimension of a rescale operation:
87/// let area: unit!(mm) = rescale(5.0m);
88/// assert_eq!(area.unsafe_value, 5000.0);
89/// # }
90/// ```
91#[proc_macro]
92pub fn proc_unit(input: TokenStream) -> TokenStream {
93    let input = parse_macro_input!(input as unit_macro::UnitMacroInput);
94    input.expand().into()
95}
96
97/// Creates a [Quantity] from a value and unit expression.
98///
99/// This macro supports both storage and nonstorage units. For nonstorage units,
100/// it automatically dispatches to the appropriate declarator trait.
101///
102/// ## Syntax
103///
104/// ```rust,ignore
105/// quantity!(value, unit_expr)
106/// quantity!(value, unit_expr, storage_type)
107/// quantity!(value, unit_expr, storage_type, brand_type)
108/// ```
109///
110/// where:
111/// - `value`: The value of the quantity
112/// - `unit_expr`: A "unit literal expression"
113///     - A "unit literal expression" is either:
114///         - An atomic unit (may include prefix):
115///             - `m`, `kg`, `s`, `A`, `K`, `mol`, `cd`, `rad`
116///         - An exponentiation of an atomic unit:
117///             - `m2`, `m^2`
118///         - A multiplication of two or more (possibly exponentiated) atomic units:
119///             - `kg.m2`, `kg * m2`
120///         - A division of two such product expressions:
121///             - `kg.m2/s2`, `kg * m2 / s^2`
122///             - There may be at most one division expression in a unit literal expression
123///             - All terms trailing the division symbol are considered to be in the denominator
124/// - `storage_type`: An optional storage type for the quantity. Defaults to `f64`.
125/// - `brand_type`: An optional brand type for the quantity. Defaults to `()`.
126///
127/// ## Examples
128///
129/// ```rust
130/// # fn main() {
131/// # use whippyunits::quantity;
132/// // Basic quantities
133/// let distance = quantity!(5.0, m);
134/// let mass = quantity!(2.5, kg);
135/// let time = quantity!(10.0, s);
136///
137/// // Compound units
138/// let velocity = quantity!(10.0, m/s);
139/// let acceleration = quantity!(9.81, m/s^2);
140/// let force = quantity!(100.0, kg*m/s^2);
141/// let energy = quantity!(50.0, kg.m2/s2);
142///
143/// // With explicit storage type
144/// let distance_f32 = quantity!(5.0, m, f32);
145/// let mass_i32 = quantity!(2, kg, i32);
146///
147/// // Complex expressions
148/// let power = quantity!(1000.0, kg.m^2/s^3);
149/// let pressure = quantity!(101325.0, kg/m.s^2);
150///
151/// // Nonstorage units (e.g., imperial units)
152/// let length = quantity!(12.0, in); // inches
153/// let mass = quantity!(1.0, lb); // pounds
154/// # }
155/// ```
156///
157/// ## Best Practice: Use Compound Unit Literal Expressions
158///
159/// For compound units, prefer using a compound unit literal expression in the macro
160/// rather than performing arithmetic in source code:
161///
162/// ```rust
163/// # fn main() {
164/// # use whippyunits::quantity;
165/// // ✅ Preferred: compound unit literal expression
166/// let velocity = quantity!(10.0, m / s);
167///
168/// // ❌ Avoid: arithmetic in source code
169/// // let velocity = quantity!(10.0, m) / quantity!(1.0, s);
170/// # }
171/// ```
172///
173/// Using compound unit literal expressions provides:
174/// - **Better rust-analyzer interaction**: The proc macro always knows the result type,
175///   enabling better IDE support and type inference
176/// - **More reliable constant folding**: The math is frontloaded at compile time,
177///   with no reliance on optimization to realize that values can be interned
178#[proc_macro]
179pub fn proc_quantity(input: TokenStream) -> TokenStream {
180    let input = parse_macro_input!(input as quantity_macro::QuantityMacroInput);
181    input.expand().into()
182}
183
184/// Access the underlying numeric value of a [Quantity](crate::Quantity).
185///
186/// Because value! explicitly specifies the target unit, this is considered a
187/// "unit-safe" operation - the type system will guarantee that the access is
188/// dimensionally coherent and the value is correctly scaled.
189///
190/// Quantities with any `Brand` other than the default `()` must specify their brand
191/// explicitly in the `value!` macro arguments; failure to do so will result in a
192/// compile error.
193///
194/// Examples:
195/// ```rust
196/// # fn main() {
197/// # use whippyunits::default_declarators::*;
198/// # use whippyunits::value;
199/// # use whippyunits::quantity;
200///
201/// let distance_f64 = quantity!(1.0, m);
202/// let val_f64: f64 = value!(distance_f64, m);   // 1.0
203/// let val_f64: f64 = value!(distance_f64, mm);  // 1000.0
204/// // let _value: f64 = value!(distance_f64, s);  // ❌ compile error (incompatible dimension)
205///
206/// let distance_i32 = quantity!(1, m, i32);
207/// let val_i32: i32 = value!(distance_i32, m, i32);   // 1
208/// let val_i32: i32 = value!(distance_i32, mm, i32);  // 1000
209/// // let _value: i32 = value!(distance_i32, s, i32);  // ❌ compile error (incompatible dimension)
210/// # }
211/// ```
212#[proc_macro]
213pub fn proc_value(input: TokenStream) -> TokenStream {
214    let input = parse_macro_input!(input as value_macro::ValueMacroInput);
215    input.expand().into()
216}
217
218#[proc_macro]
219#[doc(hidden)]
220pub fn local_unit_type(input: TokenStream) -> TokenStream {
221    let input = parse_macro_input!(input as local_unit_type_macro::LocalQuantityMacroInput);
222    input.expand().into()
223}
224
225#[proc_macro]
226pub fn define_literals(input: TokenStream) -> TokenStream {
227    define_literals_macro::define_literals(input)
228}
229
230/// Generate exponentiation lookup tables with parametric range
231/// Usage: pow_lookup!(base: 2, range: -20..=20, type: rational)
232#[proc_macro]
233#[doc(hidden)]
234pub fn pow_lookup(input: TokenStream) -> TokenStream {
235    let input = parse_macro_input!(input as pow_lookup_macro::PowLookupInput);
236    input.expand().into()
237}
238
239/// Generate π exponentiation lookup tables with rational approximation
240/// Usage: pow_pi_lookup!(range: -10..=10, type: rational)
241#[proc_macro]
242#[doc(hidden)]
243pub fn pow_pi_lookup(input: TokenStream) -> TokenStream {
244    let input = parse_macro_input!(input as pow_lookup_macro::PiPowLookupInput);
245    input.expand().into()
246}
247
248/// Generate all radian erasure implementations (both to scalar and to dimensionless quantities)
249/// Usage: generate_all_radian_erasures!()
250#[proc_macro]
251#[doc(hidden)]
252pub fn generate_all_radian_erasures(input: TokenStream) -> TokenStream {
253    let input =
254        parse_macro_input!(input as generate_all_radian_erasures_macro::AllRadianErasuresInput);
255    input.expand().into()
256}
257
258/// Generate default declarators using the source of truth from whippyunits-core
259/// Usage: generate_default_declarators!()
260#[proc_macro]
261#[doc(hidden)]
262pub fn generate_default_declarators(input: TokenStream) -> TokenStream {
263    let input =
264        parse_macro_input!(input as generate_default_declarators_macro::DefaultDeclaratorsInput);
265    input.expand().into()
266}
267
268/// Generate literals module for culit integration
269/// Usage: generate_literals_module!()
270#[proc_macro]
271#[doc(hidden)]
272pub fn generate_literals_module(input: TokenStream) -> TokenStream {
273    generate_literals_module_macro::generate_literals_module(input)
274}
275
276/// Generate local unit literals namespace with lift trace documentation
277#[proc_macro]
278#[doc(hidden)]
279pub fn generate_local_unit_literals(input: TokenStream) -> TokenStream {
280    let input =
281        parse_macro_input!(input as generate_local_unit_literals_macro::LocalUnitLiteralsInput);
282    input.expand().into()
283}
284
285/// Define a local quantity trait and implementations for a given scale and set of units.
286///
287/// This is an internal macro used by define_unit_declarators! to generate the trait definitions.
288/// Based on the original scoped_preferences.rs implementation.
289#[proc_macro]
290#[doc(hidden)]
291pub fn define_local_quantity(input: TokenStream) -> TokenStream {
292    define_local_quantity_macro::define_local_quantity(input)
293}
294
295/// Define a set of declarators that auto-convert to a given set of base units.
296///
297/// See [`define_unit_declarators`] for full documentation.
298#[proc_macro]
299pub fn define_unit_declarators(input: TokenStream) -> TokenStream {
300    let input = parse_macro_input!(input as define_unit_declarators_macro::DefineBaseUnitsInput);
301    input.expand().into()
302}
303
304/// Convert an arithmetic expression to associated type syntax (with ::Output).
305///
306/// Examples:
307/// - `output!(CO / PV)` → `<CO as Div<PV>>::Output`
308/// - `output!(CO / PV * PV)` → `<<CO as Div<PV>>::Output as Mul<PV>>::Output`
309/// - `output!((CO * T) / PV)` → `<<CO as Mul<T>>::Output as Div<PV>>::Output`
310/// - `output!(1 / T)` → `<<whippyunits::quantity::Quantity<whippyunits::quantity::Scale<whippyunits::quantity::_2<0>, whippyunits::quantity::_3<0>, whippyunits::quantity::_5<0>, whippyunits::quantity::_Pi<0>>, whippyunits::quantity::Dimension<whippyunits::quantity::_M<0>, whippyunits::quantity::_L<0>, whippyunits::quantity::_T<0>, whippyunits::quantity::_I<0>, whippyunits::quantity::_Θ<0>, whippyunits::quantity::_N<0>, whippyunits::quantity::_J<0>, whippyunits::quantity::_A<0>>, f64> as Div<T>>::Output`
311#[proc_macro]
312pub fn output(input: TokenStream) -> TokenStream {
313    use quote::quote;
314    use syn::{parse_macro_input, Expr, Lit};
315
316    /// Recursively convert an expression to associated type syntax (with ::Output)
317    fn expr_to_result_type(expr: &Expr) -> proc_macro2::TokenStream {
318        match expr {
319            Expr::Binary(bin) => {
320                let left = expr_to_result_type(&bin.left);
321                let right = expr_to_result_type(&bin.right);
322
323                match bin.op {
324                    syn::BinOp::Mul(_) => {
325                        quote! {
326                            <#left as Mul<#right>>::Output
327                        }
328                    }
329                    syn::BinOp::Div(_) => {
330                        quote! {
331                            <#left as Div<#right>>::Output
332                        }
333                    }
334                    syn::BinOp::Add(_) => {
335                        quote! {
336                            <#left as Add<#right>>::Output
337                        }
338                    }
339                    syn::BinOp::Sub(_) => {
340                        quote! {
341                            <#left as Sub<#right>>::Output
342                        }
343                    }
344                    _ => {
345                        quote! { #left }
346                    }
347                }
348            }
349            Expr::Paren(paren) => expr_to_result_type(&paren.expr),
350            Expr::Path(path) => {
351                quote! { #path }
352            }
353            Expr::Group(group) => expr_to_result_type(&group.expr),
354            Expr::Lit(lit) => {
355                // Handle literal `1` as dimensionless quantity type
356                match &lit.lit {
357                    Lit::Int(int_lit) if int_lit.base10_digits() == "1" => {
358                        quote! {
359                            whippyunits::quantity::Quantity<
360                                whippyunits::quantity::Scale<
361                                    whippyunits::quantity::_2<0>,
362                                    whippyunits::quantity::_3<0>,
363                                    whippyunits::quantity::_5<0>,
364                                    whippyunits::quantity::_Pi<0>
365                                >,
366                                whippyunits::quantity::Dimension<
367                                    whippyunits::quantity::_M<0>,
368                                    whippyunits::quantity::_L<0>,
369                                    whippyunits::quantity::_T<0>,
370                                    whippyunits::quantity::_I<0>,
371                                    whippyunits::quantity::_Θ<0>,
372                                    whippyunits::quantity::_N<0>,
373                                    whippyunits::quantity::_J<0>,
374                                    whippyunits::quantity::_A<0>
375                                >,
376                                f64
377                            >
378                        }
379                    }
380                    _ => {
381                        quote! { #expr }
382                    }
383                }
384            }
385            _ => {
386                quote! { #expr }
387            }
388        }
389    }
390
391    let input = parse_macro_input!(input as Expr);
392    let result = expr_to_result_type(&input);
393    result.into()
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    #[test]
401    fn test_parse_input() {
402        // Test that the macro can parse valid input
403        let input = "LengthOrMass, Length, Mass";
404        let parsed =
405            syn::parse_str::<define_generic_dimension_macro::DefineGenericDimensionInput>(input);
406        assert!(parsed.is_ok());
407
408        let parsed = parsed.unwrap();
409        assert_eq!(parsed.trait_name.to_string(), "LengthOrMass");
410        assert_eq!(parsed.dimension_exprs.len(), 2);
411    }
412
413    #[test]
414    fn test_expand_macro() {
415        // Test that the macro expands without panicking
416        let input = syn::parse_str::<define_generic_dimension_macro::DefineGenericDimensionInput>(
417            "LengthOrMass, Length, Mass",
418        )
419        .unwrap();
420
421        let expanded = input.expand();
422        // The expanded code should contain the trait name
423        let expanded_str = expanded.to_string();
424        assert!(expanded_str.contains("LengthOrMass"));
425        assert!(expanded_str.contains("trait"));
426    }
427}