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}