rytm_rs_macro/
lib.rs

1extern crate proc_macro;
2
3mod machine_parameters;
4mod parameter_range;
5mod util;
6
7use machine_parameters::*;
8use parameter_range::*;
9use proc_macro::TokenStream;
10use quote::{format_ident, quote};
11use syn::{
12    parse_macro_input, punctuated::Punctuated, token::Comma, DeriveInput, Error as SynError, ItemFn,
13};
14
15/// A proc macro to apply range validation for function parameters used in `rytm-rs`.
16///
17/// Please check `rytm-rs` source code for exhaustive usage examples.
18///
19/// # Example
20///
21/// ```
22/// #[parameter_range(range = "arg_name:0..=127")]
23/// #[parameter_range(range = "arg_name:0..=127:u16")]
24/// #[parameter_range(range = "arg_name[opt]:0..=127")]
25/// #[parameter_range(range = "arg_name:0..=127", range = "another_arg_name:0.4..=127.8")]
26/// ```
27#[proc_macro_attribute]
28pub fn parameter_range(args: TokenStream, input: TokenStream) -> TokenStream {
29    let input_fn = parse_macro_input!(input as ItemFn);
30
31    let args: Punctuated<RangeArg, Comma> =
32        parse_macro_input!(args with Punctuated::parse_terminated);
33
34    let mut checks = Vec::new();
35    for arg in args {
36        let param_name_ident = format_ident!("{}", arg.param_name);
37        let is_inclusive = arg.range_expr.contains("..=");
38        let range_parts: Vec<&str> = if is_inclusive {
39            arg.range_expr.split("..=").collect()
40        } else {
41            arg.range_expr.split("..").collect()
42        };
43
44        if range_parts.len() != 2 {
45            return SynError::new_spanned(
46                &param_name_ident,
47                "Invalid range expression format. Expected format: `start..=end` or `start..end`",
48            )
49            .to_compile_error()
50            .into();
51        }
52
53        let start = range_parts[0];
54        let end = range_parts[1];
55
56        let type_annotation = arg.param_type.map_or_else(
57            || quote! { _ },
58            |ty| {
59                let ty = syn::parse_str::<syn::Type>(&ty).expect("Invalid type annotation");
60                quote! { #ty }
61            },
62        );
63
64        let range_check = if is_inclusive {
65            if arg.is_optional {
66                quote! {
67                    let ___start: #type_annotation = #start.parse().expect("Invalid range start");
68                    let ___end: #type_annotation = #end.parse().expect("Invalid range end");
69
70
71                    if let Some(false) = #param_name_ident.as_ref().map(|&x| x >= ___start && x <= ___end) {
72                        return Err(RytmError::Parameter(ParameterError::Range {
73                            value: #param_name_ident.unwrap().to_string(),
74                            parameter_name: stringify!(#param_name_ident).to_string(),
75                        }));
76                    }
77                }
78            } else {
79                quote! {
80                    let ___start: #type_annotation = #start.parse().expect("Invalid range start");
81                    let ___end: #type_annotation = #end.parse().expect("Invalid range end");
82
83                    if !(___start..=___end).contains(&#param_name_ident) {
84                        return Err(RytmError::Parameter(ParameterError::Range {
85                            value: #param_name_ident.to_string(),
86                            parameter_name: stringify!(#param_name_ident).to_string(),
87                        }));
88                    }
89                }
90            }
91        } else if arg.is_optional {
92            quote! {
93                let ___start: #type_annotation = #start.parse().expect("Invalid range start");
94                let ___end: #type_annotation = #end.parse().expect("Invalid range end");
95
96
97                if let Some(false) = #param_name_ident.as_ref().map(|&x| x >= ___start && x < ___end) {
98                    return Err(RytmError::Parameter(ParameterError::Range {
99                        value: #param_name_ident.unwrap().to_string(),
100                        parameter_name: stringify!(#param_name_ident).to_string(),
101                    }));
102                }
103            }
104        } else {
105            quote! {
106                let ___start: #type_annotation = #start.parse().expect("Invalid range start");
107                let ___end: #type_annotation = #end.parse().expect("Invalid range end");
108
109                if !(___start..___end).contains(&#param_name_ident) {
110                    return Err(RytmError::Parameter(ParameterError::Range {
111                        value: #param_name_ident.to_string(),
112                        parameter_name: stringify!(#param_name_ident).to_string(),
113                    }));
114                }
115            }
116        };
117
118        checks.push(range_check);
119    }
120
121    let ItemFn {
122        attrs,
123        vis,
124        sig,
125        block,
126    } = input_fn;
127
128    let result = quote! {
129        #(#attrs)* #vis #sig {
130            #(#checks)*
131            #block
132        }
133    };
134
135    result.into()
136}
137
138/// A proc macro to implement normal and parameter lock getter, setter and clearer methods mor machine parameter structs.
139///
140/// `#<number>` annotations denote synth parameter indices used internally.
141///
142/// The macro also generates a `apply_to_raw_sound_values` method which is used to apply the parameter values to the raw sound struct.
143///
144/// Please check `rytm-rs` source code for exhaustive usage examples.
145///
146/// # Example
147///
148/// ```
149/// #[machine_parameters(
150///     lev: "0..=127" #1,
151///     tun: "-32.0..=32.0" #2,
152///     dec: "0..=127" #3,
153///     hld: "0..=127" #4,
154///     swt: "0..=127" #5,
155///     swd: "0..=127" #6,
156///     wav: "0..=2" #7,
157///     tra: "0..=127" #8,
158/// )]
159/// ```
160#[proc_macro_attribute]
161pub fn machine_parameters(args: TokenStream, input: TokenStream) -> TokenStream {
162    let args = parse_macro_input!(args as ParameterArgs);
163    let input_struct = parse_macro_input!(input as DeriveInput);
164    let struct_name = input_struct.ident.clone();
165
166    let methods = args.0.iter().map(|arg| {
167        // let (param_type, return_type) = determine_types(&arg.range);
168        let setter_and_plock_methods =
169            generate_setter_and_plock_methods_with_range_check(arg, &struct_name);
170        let getter = generate_getter(arg, &struct_name);
171        quote! { #setter_and_plock_methods #getter }
172    });
173
174    let apply_to_raw_sound_values_inner =
175        args.0.iter().map(generate_apply_to_raw_sound_values_inner);
176
177    let result = quote! {
178        #input_struct
179
180        impl #struct_name {
181            pub(crate) fn apply_to_raw_sound_values(&self, raw_sound: &mut ar_sound_t) {
182                #(#apply_to_raw_sound_values_inner)*
183            }
184        }
185
186        #(#methods)*
187    };
188
189    result.into()
190}