microscpi_macros/
lib.rs

1//! This crate provides procedural macros for the microscpi library.
2//!
3//! The main macro is `interface`, which processes an implementation block
4//! to generate the code needed for an SCPI command interpreter.
5use std::rc::Rc;
6
7use proc_macro::TokenStream;
8use proc_macro_error2::{abort, proc_macro_error};
9use quote::{format_ident, quote};
10use syn::punctuated::Punctuated;
11use syn::spanned::Spanned;
12use syn::token::Comma;
13use syn::{Attribute, Expr, Ident, ImplItemFn, ItemImpl, Lit, Path, Type, parse_macro_input};
14
15mod tree;
16
17use microscpi_common::Command;
18use tree::Tree;
19
20/// Represents a handler for an SCPI command.
21///
22/// This can be either:
23/// - A user-defined function within the impl block
24/// - A standard function from the microscpi library
25#[derive(Clone)]
26enum CommandHandler {
27    /// A user-defined function identified by its identifier
28    UserFunction(Ident),
29    /// A standard function from the microscpi library, identified by its path
30    StandardFunction(&'static str),
31}
32
33impl CommandHandler {
34    /// Returns the span of the command handler for error reporting.
35    fn span(&self) -> proc_macro2::Span {
36        match self {
37            CommandHandler::UserFunction(ident) => ident.span(),
38            CommandHandler::StandardFunction(_) => proc_macro2::Span::call_site(),
39        }
40    }
41}
42
43/// Configuration options for the generated SCPI interface.
44#[derive(Default)]
45struct Config {
46    /// Whether to include standard error commands
47    pub error_commands: bool,
48    /// Whether to include standard SCPI commands
49    pub standard_commands: bool,
50    /// Whether to include status commands
51    pub status_commands: bool,
52}
53
54/// Defines a complete SCPI command with its handler function and arguments.
55#[derive(Clone)]
56struct CommandDefinition {
57    /// Unique identifier for this command in the command tree
58    pub id: Option<usize>,
59    /// The parsed SCPI command
60    pub command: Command,
61    /// The function that handles this command
62    pub handler: CommandHandler,
63    /// Types of the expected arguments
64    pub args: Vec<Type>,
65    /// Whether the handler is an async function
66    pub future: bool,
67}
68
69impl CommandDefinition {
70    /// Generates the argument expressions for calling the command handler.
71    ///
72    /// This creates code to extract and convert each argument from the input
73    /// argument list using the appropriate conversion.
74    fn args(&self) -> Punctuated<Expr, Comma> {
75        self.args
76            .iter()
77            .enumerate()
78            .map(|(id, _arg)| -> Expr {
79                syn::parse_quote! {
80                    args.get(#id).unwrap().try_into()?
81                }
82            })
83            .collect()
84    }
85
86    fn call(&self) -> proc_macro2::TokenStream {
87        let command_id = self.id;
88        let arg_count = self.args.len();
89        let args = self.args();
90
91        let fn_call = match &self.handler {
92            CommandHandler::UserFunction(ident) => {
93                let func = ident.clone();
94                quote! { self.#func(#args) }
95            }
96            CommandHandler::StandardFunction(path) => {
97                let path: Path = syn::parse(path.parse().unwrap()).unwrap();
98                quote! { ::microscpi::#path(self, #args) }
99            }
100        };
101
102        let fn_call = if self.future {
103            quote! { #fn_call.await? }
104        } else {
105            quote! { #fn_call? }
106        };
107
108        quote! {
109            #command_id => {
110                if args.len() != #arg_count {
111                    Err(::microscpi::Error::UnexpectedNumberOfParameters)
112                }
113                else {
114                    let result = #fn_call;
115                    result.write_response(response)
116                }
117            }
118        }
119    }
120}
121
122impl CommandDefinition {
123    /// Parses a function and its `scpi` attribute to create a CommandDefinition.
124    ///
125    /// # Arguments
126    /// * `func` - The function to parse
127    /// * `attr` - The `scpi` attribute to parse
128    ///
129    /// # Returns
130    /// A CommandDefinition if the attribute contains a valid SCPI command.
131    ///
132    /// # Errors
133    /// Returns an error if the attribute contains an invalid SCPI command name.
134    fn parse(func: &ImplItemFn, attr: &Attribute) -> syn::Result<CommandDefinition> {
135        let mut cmd: Option<String> = None;
136
137        attr.parse_nested_meta(|meta| {
138            if meta.path.is_ident("cmd") {
139                if let Lit::Str(name) = meta.value()?.parse()? {
140                    cmd = Some(name.value());
141                    Ok(())
142                } else {
143                    abort!(
144                        meta.path.span(),
145                        "SCPI command name must be a string literal"
146                    )
147                }
148            } else {
149                Ok(())
150            }
151        })?;
152
153        // Analyze the function signature to collect argument types
154        let args = func
155            .sig
156            .inputs
157            .iter()
158            .filter_map(|arg| match arg {
159                syn::FnArg::Typed(arg_type) => Some(*arg_type.ty.clone()),
160                syn::FnArg::Receiver(_) => None,
161            })
162            .collect();
163
164        if let Some(cmd) = cmd {
165            Ok(CommandDefinition {
166                id: None,
167                command: Command::try_from(cmd.as_str())
168                    .map_err(|_| syn::Error::new(attr.span(), "Invalid SCPI command syntax"))?,
169                handler: CommandHandler::UserFunction(func.sig.ident.to_owned()),
170                args,
171                future: func.sig.asyncness.is_some(),
172            })
173        } else {
174            abort!(
175                attr.span(),
176                "Missing `cmd` attribute in SCPI command. Expected: #[scpi(cmd = \"COMMAND:NAME\")]"
177            )
178        }
179    }
180}
181
182struct CommandSet {
183    commands: Vec<Rc<CommandDefinition>>,
184}
185
186impl CommandSet {
187    pub fn new() -> Self {
188        Self {
189            commands: Vec::new(),
190        }
191    }
192
193    pub fn push(&mut self, mut command: CommandDefinition) {
194        command.id = Some(self.commands.len());
195        self.commands.push(Rc::new(command));
196    }
197
198    /// Extracts all SCPI command functions from an `impl` block.
199    ///
200    /// This function processes all methods in the provided implementation block,
201    /// looking for those with the `#[scpi]` attribute, and converts them to
202    /// CommandDefinition objects.
203    ///
204    /// # Arguments
205    /// * `input` - The implementation item of the struct from which to extract the SCPI
206    ///   commands.
207    ///
208    /// # Returns
209    /// A vector containing all found command definitions.
210    ///
211    /// # Errors
212    /// Returns an error if any SCPI attribute fails to parse.
213    fn extract_commands(&mut self, input: &mut ItemImpl) -> Result<(), syn::Error> {
214        for item in input.items.iter_mut() {
215            if let syn::ImplItem::Fn(item_fn) = item {
216                // Find all SCPI attributes for this function, parse them and then remove
217                // them from the list of attributes, so the compiler does not complain about
218                // unknown attributes.
219                let mut idx = 0;
220                while idx < item_fn.attrs.len() {
221                    if item_fn.attrs[idx].path().is_ident("scpi") {
222                        let attr = item_fn.attrs.remove(idx);
223                        self.push(CommandDefinition::parse(item_fn, &attr)?);
224                    } else {
225                        idx += 1;
226                    }
227                }
228            }
229        }
230        Ok(())
231    }
232}
233
234impl AsRef<[Rc<CommandDefinition>]> for CommandSet {
235    fn as_ref(&self) -> &[Rc<CommandDefinition>] {
236        self.commands.as_ref()
237    }
238}
239
240/// Macro attribute to define an SCPI interface.
241///
242/// This attribute processes an `impl` block and registers the SCPI commands
243/// defined within it. It generates the code needed to implement the
244/// `microscpi::Interface` trait, including the command tree and command handler
245/// dispatch logic.
246///
247/// # Options
248///
249/// The interface can be configured with additional options:
250///
251/// ```ignore
252/// use microscpi::Interface;
253///
254/// #[microscpi::interface(StandardCommands, ErrorCommands, export = "commands.json")]
255/// impl ExampleInterface {
256///     // ...
257/// }
258/// ```
259///
260/// Available options:
261/// - `StandardCommands`: Add standard SCPI commands (e.g., `SYSTem:VERSion?`)
262/// - `ErrorCommands`: Add error-related commands (e.g., `SYSTem:ERRor:[NEXT]?`)
263/// - `StatusCommands`: Add status-related commands (e.g., `*OPC`, `*CLS`)
264///
265#[proc_macro_error]
266#[proc_macro_attribute]
267pub fn interface(attr: TokenStream, item: TokenStream) -> TokenStream {
268    let attrs: Punctuated<Path, Comma> = parse_macro_input!(attr with Punctuated::parse_terminated);
269    let mut input_impl = parse_macro_input!(item as ItemImpl);
270
271    let mut config = Config::default();
272
273    // Process configuration options from the attribute parameters
274    for attr in attrs {
275        if attr.is_ident("ErrorCommands") {
276            config.error_commands = true;
277        } else if attr.is_ident("StandardCommands") {
278            config.standard_commands = true;
279        } else if attr.is_ident("StatusCommands") {
280            config.status_commands = true;
281        } else {
282            abort!(attr.span(), "Unknown SCPI interface option.");
283        }
284    }
285
286    let mut command_set = CommandSet::new();
287
288    if config.standard_commands {
289        command_set.push(CommandDefinition {
290            id: None,
291            args: Vec::new(),
292            command: Command::try_from("SYSTem:VERSion?").unwrap(),
293            handler: CommandHandler::StandardFunction("StandardCommands::system_version"),
294            future: false,
295        });
296    }
297
298    if config.error_commands {
299        command_set.push(CommandDefinition {
300            id: None,
301            args: Vec::new(),
302            command: Command::try_from("SYSTem:ERRor:[NEXT]?").unwrap(),
303            handler: CommandHandler::StandardFunction("ErrorCommands::system_error_next"),
304            future: false,
305        });
306
307        command_set.push(CommandDefinition {
308            id: None,
309            args: Vec::new(),
310            command: Command::try_from("SYSTem:ERRor:COUNt?").unwrap(),
311            handler: CommandHandler::StandardFunction("ErrorCommands::system_error_count"),
312            future: false,
313        });
314    }
315
316    if config.status_commands {
317        command_set.push(CommandDefinition {
318            id: None,
319            args: Vec::new(),
320            command: Command::try_from("*OPC").unwrap(),
321            handler: CommandHandler::StandardFunction("StatusCommands::set_operation_complete"),
322            future: false,
323        });
324
325        command_set.push(CommandDefinition {
326            id: None,
327            args: Vec::new(),
328            command: Command::try_from("*OPC?").unwrap(),
329            handler: CommandHandler::StandardFunction("StatusCommands::operation_complete"),
330            future: false,
331        });
332
333        command_set.push(CommandDefinition {
334            id: None,
335            args: Vec::new(),
336            command: Command::try_from("*CLS").unwrap(),
337            handler: CommandHandler::StandardFunction("StatusCommands::clear_event_status"),
338            future: false,
339        });
340
341        command_set.push(CommandDefinition {
342            id: None,
343            args: Vec::new(),
344            command: Command::try_from("*ESE?").unwrap(),
345            handler: CommandHandler::StandardFunction("StatusCommands::event_status_enable"),
346            future: false,
347        });
348
349        command_set.push(CommandDefinition {
350            id: None,
351            args: vec![Type::Verbatim(quote! { u8 })],
352            command: Command::try_from("*ESE").unwrap(),
353            handler: CommandHandler::StandardFunction("StatusCommands::set_event_status_enable"),
354            future: false,
355        });
356
357        command_set.push(CommandDefinition {
358            id: None,
359            args: Vec::new(),
360            command: Command::try_from("*ESR?").unwrap(),
361            handler: CommandHandler::StandardFunction("StatusCommands::event_status_register"),
362            future: false,
363        });
364
365        command_set.push(CommandDefinition {
366            id: None,
367            args: Vec::new(),
368            command: Command::try_from("*STB?").unwrap(),
369            handler: CommandHandler::StandardFunction("StatusCommands::status_byte"),
370            future: false,
371        });
372
373        command_set.push(CommandDefinition {
374            id: None,
375            args: Vec::new(),
376            command: Command::try_from("*SRE?").unwrap(),
377            handler: CommandHandler::StandardFunction("StatusCommands::status_byte_enable"),
378            future: false,
379        });
380
381        command_set.push(CommandDefinition {
382            id: None,
383            args: vec![Type::Verbatim(quote! { u8 })],
384            command: Command::try_from("*SRE").unwrap(),
385            handler: CommandHandler::StandardFunction("StatusCommands::set_status_byte_enable"),
386            future: false,
387        });
388    }
389
390    // Extract user-defined SCPI commands from the implementation block
391    if let Err(error) = command_set.extract_commands(&mut input_impl) {
392        return error.into_compile_error().into();
393    }
394
395    let mut tree = Tree::new();
396
397    // Insert all commands into the command tree
398    for cmd in command_set.as_ref().iter() {
399        if let Err(error) = tree.insert(cmd.clone()) {
400            abort!(
401                cmd.handler.span(),
402                "Failed to register SCPI command '{}': {}",
403                cmd.command.canonical_path(),
404                error
405            )
406        }
407    }
408
409    let command_items: Vec<proc_macro2::TokenStream> =
410        command_set.as_ref().iter().map(|cmd| cmd.call()).collect();
411
412    let mut nodes: Vec<proc_macro2::TokenStream> = Vec::new();
413
414    for (node_id, cmd_node) in tree.items {
415        let node_name = format_ident!("SCPI_NODE_{}", node_id);
416
417        let entries = cmd_node.children.iter().map(|(name, node_id)| {
418            let reference = format_ident!("SCPI_NODE_{}", node_id);
419            quote!((#name, &#reference))
420        });
421
422        let command = if let Some(command_id) = cmd_node.command.map(|cmd_def| cmd_def.id) {
423            quote! { Some(#command_id) }
424        } else {
425            quote! { None }
426        };
427        let query = if let Some(command_id) = cmd_node.query.map(|cmd_def| cmd_def.id) {
428            quote! { Some(#command_id) }
429        } else {
430            quote! { None }
431        };
432
433        let node_item = quote! {
434            static #node_name: ::microscpi::Node = ::microscpi::Node {
435                children: &[
436                    #(#entries),*
437                ],
438                command: #command,
439                query: #query
440            };
441        };
442
443        nodes.push(node_item);
444    }
445
446    let impl_ty = input_impl.self_ty.clone();
447
448    let mut interface_impl: ItemImpl = syn::parse_quote! {
449        impl ::microscpi::Interface for #impl_ty {
450            fn root_node(&self) -> &'static ::microscpi::Node {
451                &SCPI_NODE_0
452            }
453            async fn execute_command<'a>(
454                &'a mut self,
455                command_id: ::microscpi::CommandId,
456                args: &[::microscpi::Value<'a>],
457                response: &mut impl ::microscpi::Write
458            ) -> Result<(), ::microscpi::Error> {
459                use ::microscpi::Response;
460                match command_id {
461                    #(#command_items),*,
462                    _ => Err(::microscpi::Error::UndefinedHeader)
463                }
464           }
465        }
466    };
467
468    // Copy the generics from the main implementation
469    interface_impl.generics = input_impl.generics.clone();
470
471    quote! {
472        #(#nodes)*
473        #input_impl
474        #interface_impl
475    }
476    .into()
477}