public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Max R. Carrara" <m.carrara@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct
Date: Fri, 24 Jul 2026 16:49:06 +0200	[thread overview]
Message-ID: <20260724144914.658730-5-m.carrara@proxmox.com> (raw)
In-Reply-To: <20260724144914.658730-1-m.carrara@proxmox.com>

The handling of function signature inputs adds a lot of mutable state
to the body of the `handle_function()` fn, which makes it somewhat
hard to track when that state is actually mutated throughout the
function.

Therefore, introduce the private `FnInputs` struct and heave all of
the fn signature input handling into `FnInputs::from_signature()` to
limit the scope where mutability occurs. Additionally, use an inline
`State` struct inside `FnInputs::from_signature()` in order to make it
explicit at a glance when mutable state is in fact mutated.

Also introduce the `TrailingType` enum and use it inside `FnInputs`
instead of the `had_list_param` boolean in order to make adding such
proc macro parameter attributes a little easier in the future.

Take the opportunity to improve the overall signature inputs handling
logic by making use of some more pattern matching and using `break`s
from labeled blocks [1] instead of an if-elseif-else expression when
generating the code for the `None` handling.

Adapt the `gen_prototype()` function and make it take a reference to
`&FnInputs` instead, and also use pattern matching when defining the
string for the Perl subroutine prototype. Avoid shadowing the
`arg_count` variable too by renaming the parameter to
`total_arg_count`.

Adapt the `handle_return_kind()` function and make it take a reference
to `&FnInputs` as well, removing obsolete parameters in the process.
Adapt its body accordingly.

[1] https://blog.rust-lang.org/2022/11/03/Rust-1.65.0/#break-from-labeled-blocks

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 perlmod-macro/src/function.rs | 326 ++++++++++++++++++++++------------
 1 file changed, 208 insertions(+), 118 deletions(-)

diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index 3ca7bd0..f5415c5 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -210,6 +210,171 @@ fn deserialized_argument_code(
     }
 }
 
+enum TrailingType {
+    List,
+}
+
+impl TrailingType {
+    fn as_str(&self) -> &'static str {
+        match self {
+            Self::List => "list",
+        }
+    }
+}
+
+struct FnInputs {
+    trailing_options: usize,
+    extract_arguments: TokenStream,
+    deserialized_arguments: TokenStream,
+    passed_arguments: TokenStream,
+    cv_arg_param: TokenStream,
+    trailing_type: Option<TrailingType>,
+}
+
+impl FnInputs {
+    fn from_signature(
+        signature: &mut syn::Signature,
+        arguments_name: &Ident,
+    ) -> Result<Self, Error> {
+        #[derive(Default)]
+        struct State {
+            trailing_options: usize,
+            extract_arguments: TokenStream,
+            deserialized_arguments: TokenStream,
+            passed_arguments: TokenStream,
+            cv_arg_param: TokenStream,
+            trailing_type: Option<TrailingType>,
+        }
+
+        let mut state = State::default();
+
+        let span = signature.ident.span();
+
+        for arg in &mut signature.inputs {
+            let arg_attr = ArgumentAttr::new_from_fn_arg(arg)?;
+
+            let arg_type: &syn::Type = arg_attr.pat_type.ty.as_ref();
+
+            let arg_name = {
+                let pattern: &syn::Pat = arg_attr.pat_type.pat.as_ref();
+                match pattern {
+                    syn::Pat::Ident(ident) => {
+                        if ident.by_ref.is_some() {
+                            bail!(ident => "xsub does not support by-ref parameters");
+                        }
+                        if ident.subpat.is_some() {
+                            bail!(ident => "xsub does not support sub-patterns on parameters");
+                        }
+                        &ident.ident
+                    }
+                    _ => bail!(pattern => "xsub does not support this kind of parameter"),
+                }
+            };
+
+            if let Some(ref attr_type) = arg_attr.attr_type {
+                match attr_type {
+                    ArgumentAttrType::Raw => {}
+                    ArgumentAttrType::TryFromRef => {}
+                    ArgumentAttrType::CVPtr(cv_span) => {
+                        if !state.cv_arg_param.is_empty() {
+                            bail!(*cv_span, "only 1 'cv' parameter allowed");
+                        }
+
+                        state.cv_arg_param = quote_spanned! { span=> #arg_name: #arg_type };
+                        if state.passed_arguments.is_empty() {
+                            state
+                                .passed_arguments
+                                .extend(quote_spanned! { span=> #arg_name });
+                        } else {
+                            state
+                                .passed_arguments
+                                .extend(quote_spanned! { span=> , #arg_name });
+                        }
+
+                        continue;
+                    }
+                    ArgumentAttrType::TrailingList(trailing_span) => {
+                        if let Some(trailing_type) = state.trailing_type {
+                            bail!(
+                                *trailing_span,
+                                "only 1 parameter for trailing values allowed, already got '{}'",
+                                trailing_type.as_str(),
+                            );
+                        }
+
+                        state.trailing_type = Some(TrailingType::List);
+                    }
+                }
+            }
+
+            let extracted_name = Ident::new(&format!("extracted_arg_{arg_name}"), arg_name.span());
+            let deserialized_name =
+                Ident::new(&format!("deserialized_arg_{arg_name}"), arg_name.span());
+
+            let missing_message = syn::LitStr::new(
+                &format!("missing required parameter: '{arg_name}'\n"),
+                arg_name.span(),
+            );
+
+            let none_handling = 'handling: {
+                if is_option_type(arg_type).is_some() {
+                    state.trailing_options += 1;
+                    break 'handling quote_spanned! { span=> ::perlmod::Value::new_undef(), };
+                }
+
+                if matches!(arg_attr.attr_type, Some(ArgumentAttrType::TrailingList(_))) {
+                    break 'handling TokenStream::new();
+                }
+
+                // only count the trailing options
+                state.trailing_options = 0;
+                quote_spanned! { span=>
+                    {
+                        return Err(::perlmod::Value::new_string(#missing_message)
+                            .into_mortal()
+                            .into_raw());
+                    }
+                }
+            };
+
+            let arg_code = extract_argument_code(
+                &arg_attr,
+                span,
+                arguments_name,
+                &extracted_name,
+                none_handling,
+            );
+
+            let de_arg_code = deserialized_argument_code(
+                &arg_attr,
+                span,
+                arg_type,
+                &deserialized_name,
+                extracted_name,
+            );
+
+            let passed_arg = if state.passed_arguments.is_empty() {
+                quote_spanned! { span=> #deserialized_name }
+            } else {
+                quote_spanned! { span=> , #deserialized_name }
+            };
+
+            state.extract_arguments.extend(arg_code);
+            state.deserialized_arguments.extend(de_arg_code);
+            state.passed_arguments.extend(passed_arg)
+        }
+
+        Ok(Self {
+            trailing_options: state.trailing_options,
+            extract_arguments: state.extract_arguments,
+            deserialized_arguments: state.deserialized_arguments,
+            passed_arguments: state.passed_arguments,
+            cv_arg_param: state.cv_arg_param,
+            trailing_type: state.trailing_type,
+        })
+    }
+}
+
 struct Return {
     result: bool,
     value: ReturnValue,
@@ -255,99 +420,7 @@ pub fn handle_function(
 
     let arguments_name = syn::Ident::new("args", name.span());
 
-    let mut trailing_options = 0;
-    let mut extract_arguments = TokenStream::new();
-    let mut deserialized_arguments = TokenStream::new();
-    let mut passed_arguments = TokenStream::new();
-    let mut cv_arg_param = TokenStream::new();
-    let mut had_list_param = false;
-    for arg in &mut func.sig.inputs {
-        let arg_attr = ArgumentAttr::new_from_fn_arg(arg)?;
-
-        if let Some(ArgumentAttrType::TrailingList(list_span)) = arg_attr.attr_type {
-            if had_list_param {
-                bail!(list_span, "only 1 #[list] parameter allowed");
-            }
-
-            had_list_param = true;
-        }
-
-        let arg_name = match &*arg_attr.pat_type.pat {
-            syn::Pat::Ident(ident) => {
-                if ident.by_ref.is_some() {
-                    bail!(ident => "xsub does not support by-ref parameters");
-                }
-                if ident.subpat.is_some() {
-                    bail!(ident => "xsub does not support sub-patterns on parameters");
-                }
-                &ident.ident
-            }
-            _ => bail!(&arg_attr.pat_type.pat => "xsub does not support this kind of parameter"),
-        };
-
-        let arg_type = &*arg_attr.pat_type.ty;
-
-        if let Some(ArgumentAttrType::CVPtr(cv_span)) = arg_attr.attr_type {
-            if !cv_arg_param.is_empty() {
-                bail!(cv_span, "only 1 'cv' parameter allowed");
-            }
-            cv_arg_param = quote_spanned! { span=> #arg_name: #arg_type };
-            if passed_arguments.is_empty() {
-                passed_arguments.extend(quote_spanned! { span=> #arg_name });
-            } else {
-                passed_arguments.extend(quote_spanned! { span=> , #arg_name });
-            }
-            continue;
-        }
-
-        let extracted_name = Ident::new(&format!("extracted_arg_{arg_name}"), arg_name.span());
-        let deserialized_name =
-            Ident::new(&format!("deserialized_arg_{arg_name}"), arg_name.span());
-
-        let missing_message = syn::LitStr::new(
-            &format!("missing required parameter: '{arg_name}'\n"),
-            arg_name.span(),
-        );
-
-        let none_handling = if is_option_type(arg_type).is_some() {
-            trailing_options += 1;
-            quote_spanned! { span=> ::perlmod::Value::new_undef(), }
-        } else if matches!(arg_attr.attr_type, Some(ArgumentAttrType::TrailingList(_))) {
-            TokenStream::new()
-        } else {
-            // only count the trailing options;
-            trailing_options = 0;
-            quote_spanned! { span=>
-                {
-                    return Err(::perlmod::Value::new_string(#missing_message)
-                        .into_mortal()
-                        .into_raw());
-                }
-            }
-        };
-
-        extract_arguments.extend(extract_argument_code(
-            &arg_attr,
-            span,
-            &arguments_name,
-            &extracted_name,
-            none_handling,
-        ));
-
-        deserialized_arguments.extend(deserialized_argument_code(
-            &arg_attr,
-            span,
-            arg_type,
-            &deserialized_name,
-            extracted_name,
-        ));
-
-        if passed_arguments.is_empty() {
-            passed_arguments.extend(quote_spanned! { span=> #deserialized_name });
-        } else {
-            passed_arguments.extend(quote_spanned! { span=> , #deserialized_name });
-        }
-    }
+    let inputs = FnInputs::from_signature(&mut func.sig, &arguments_name)?;
 
     let has_return_value = match &func.sig.output {
         syn::ReturnType::Default => Return {
@@ -370,12 +443,12 @@ pub fn handle_function(
         },
     };
 
-    let finalize_arguments = if !had_list_param {
+    let finalize_arguments = if !inputs.trailing_type.is_some() {
         let too_many_args_error = syn::LitStr::new(
             &format!(
                 "too many parameters for function '{}', (expected {})\n",
                 name,
-                func.sig.inputs.len() - (!cv_arg_param.is_empty()) as usize
+                func.sig.inputs.len() - (!inputs.cv_arg_param.is_empty()) as usize
             ),
             Span::call_site(),
         );
@@ -397,13 +470,12 @@ pub fn handle_function(
         wrapper_func,
     } = handle_return_kind(
         &attr,
+        &inputs,
         has_return_value,
         &name,
         &xs_name,
         &impl_xs_name,
-        passed_arguments,
         export_public,
-        !cv_arg_param.is_empty(),
     )?;
 
     let visibility_action = check_visibility(&func);
@@ -414,6 +486,10 @@ pub fn handle_function(
         #wrapper_func
     };
 
+    let cv_arg_param = &inputs.cv_arg_param;
+    let extract_arguments = &inputs.extract_arguments;
+    let deserialized_arguments = &inputs.deserialized_arguments;
+
     tokens.extend(quote_spanned! { span=>
         #[inline(never)]
         #[allow(non_snake_case)]
@@ -451,36 +527,48 @@ pub fn handle_function(
         perl_name: attr.perl_name,
         xs_name,
         tokens,
-        prototype: attr.prototype.or_else(|| {
-            Some(gen_prototype(
-                func.sig.inputs.len(),
-                trailing_options,
-                had_list_param,
-            ))
-        }),
+        prototype: attr
+            .prototype
+            .or_else(|| Some(gen_prototype(func.sig.inputs.len(), &inputs))),
     })
 }
 
-fn gen_prototype(arg_count: usize, trailing_options: usize, had_list_param: bool) -> String {
-    let arg_count = arg_count - trailing_options - (had_list_param as usize);
+fn gen_prototype(total_arg_count: usize, inputs: &FnInputs) -> String {
+    let trailing_options = inputs.trailing_options;
+    let trailing_type = inputs.trailing_type.as_ref();
+
+    let arg_count = total_arg_count - trailing_options - (trailing_type.is_some() as usize);
 
     let mut proto = String::with_capacity(arg_count + trailing_options + 1);
 
     for _ in 0..arg_count {
         proto.push('$');
     }
-    if trailing_options > 0 {
-        proto.push(';');
-        for _ in 0..trailing_options {
-            proto.push('$');
+
+    match (trailing_options, trailing_type) {
+        (1.., ty) => {
+            proto.push(';');
+
+            for _ in 0..trailing_options {
+                proto.push('$');
+            }
+
+            match ty {
+                Some(TrailingType::List) => proto.push('@'),
+                None => {}
+            }
+
+            proto
         }
-        if had_list_param {
-            proto.push('@');
+        (0, ty) => {
+            match ty {
+                Some(TrailingType::List) => proto.push_str(";@"),
+                None => {}
+            }
+
+            proto
         }
-    } else if had_list_param {
-        proto.push_str(";@");
     }
-    proto
 }
 
 struct ReturnHandling {
@@ -492,16 +580,17 @@ struct ReturnHandling {
 #[allow(clippy::too_many_arguments)]
 fn handle_return_kind(
     attr: &FunctionAttrs,
+    inputs: &FnInputs,
     ret: Return,
     name: &Ident,
     xs_name: &Ident,
     impl_xs_name: &Ident,
-    passed_arguments: TokenStream,
     export_public: Option<&syn::Visibility>,
-    cv_arg: bool,
 ) -> Result<ReturnHandling, Error> {
     let span = name.span();
 
+    let passed_arguments = &inputs.passed_arguments;
+
     let return_type;
     let mut handle_return;
     let wrapper_func;
@@ -511,7 +600,8 @@ fn handle_return_kind(
         None => quote_spanned! { span=> #[allow(non_snake_case)] },
     };
 
-    let (cv_arg_name, cv_arg_passed) = if cv_arg {
+    let has_cv_arg = !inputs.cv_arg_param.is_empty();
+    let (cv_arg_name, cv_arg_passed) = if has_cv_arg {
         (
             quote_spanned! { span=> cv },
             quote_spanned! { span=> ::perlmod::Value::from_raw_ref(cv as *mut ::perlmod::ffi::SV) },
-- 
2.47.3





  parent reply	other threads:[~2026-07-24 14:50 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 1/5] macro: function: make argument code generation helpers standalone fns Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 2/5] macro: function: make argument attribute handling more extendable Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 3/5] macro: function: explicitly pass identifier for generated arg iter Max R. Carrara
2026-07-24 14:49 ` Max R. Carrara [this message]
2026-07-24 14:49 ` [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute Max R. Carrara

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260724144914.658730-5-m.carrara@proxmox.com \
    --to=m.carrara@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal