all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute
@ 2026-07-24 14:49 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
                   ` (6 more replies)
  0 siblings, 7 replies; 11+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
  To: pve-devel

perlmod: add #[hash] parameter attribute - v2
=============================================

This series implements a `#[hash]` parameter similar to the existing
`#[list]` one, allowing us to create APIs like
`foo("arg", key => "value")` directly from Rust.

In other words, this essentially mirrors how a sub with `%params` as its
last parameter would work.

For example, this is one of the functions called by the tests that are
added in this series:

    #[export]
    fn trailing_hash(first: u32, #[hash] rest: HashMap<u32, u32>) -> String {
        format!("first={first}, rest is {rest:?}")
    }

This can then be called e.g. like this:

    let $res = TestLib::Hello::trailing_hash(42, 1337 => 123);

Changes Since v1
----------------

- Drop patches that were already applied (thanks!)

- Call it "parameter attribute", not "parameter"

- Take Wolfgang's feedback (thanks!) on patch #3 of v1 into account and
  refactor the majority of `perlmod-macro/src/function.rs` before
  actually implementing the #[hash] parameter attribute.

  This means that all patches but the last just massage `function.rs`
  into a shape that makes it convenient to add and debug new parameter
  attributes. The overall logic was improved as well, making it more
  clear / explicit what's happening in a given block of code.

The tests and docstrings added in the last patch are otherwise kept
identical, including the implementation of `ParamsIter<I>`.

Previous Versions
-----------------

v1: https://lore.proxmox.com/pve-devel/20260720152541.524463-1-m.carrara@proxmox.com/

Summary of Changes
------------------


perlmod:

Max R. Carrara (5):
  macro: function: make argument code generation helpers standalone fns
  macro: function: make argument attribute handling more extendable
  macro: function: explicitly pass identifier for generated arg iter
  macro: function: move signature inputs handling into helper struct
  perlmod, macro: add #[hash] parameter attribute

 perlmod-macro/src/function.rs | 648 ++++++++++++++++++++++------------
 perlmod/src/de.rs             |  44 +++
 perlmod/src/lib.rs            |   5 +
 testlib-tests/01-hello.t      |  46 ++-
 testlib/src/lib.rs            |  24 +-
 5 files changed, 538 insertions(+), 229 deletions(-)


Summary over all repositories:
  5 files changed, 538 insertions(+), 229 deletions(-)

-- 
Generated by murpp 0.11.0




^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH perlmod v2 1/5] macro: function: make argument code generation helpers standalone fns
  2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
@ 2026-07-24 14:49 ` Max R. Carrara
  2026-07-24 14:49 ` [PATCH perlmod v2 2/5] macro: function: make argument attribute handling more extendable Max R. Carrara
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
  To: pve-devel

There is not really much of a reason to associate them with
`ArgumentAttrs` from a semantic standpoint, so make those helpers
standalone functions instead.

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

diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index 8b5d689..9d4a4af 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -86,80 +86,80 @@ impl ArgumentAttrs {
             }
         }
     }
+}
 
-    fn extract_argument_code(
-        &self,
-        span: Span,
-        extracted_name: &Ident,
-        none_handling: TokenStream,
-    ) -> TokenStream {
-        if self.list.is_some() {
-            quote_spanned! { span=>
-                let #extracted_name = args.map(::perlmod::Value::from);
-            }
-        } else {
-            quote_spanned! { span=>
-                let #extracted_name: ::perlmod::Value = match args.next() {
-                    Some(arg) => ::perlmod::Value::from(arg),
-                    None => #none_handling
-                };
-            }
+fn extract_argument_code(
+    arg_attr: &ArgumentAttrs,
+    span: Span,
+    extracted_name: &Ident,
+    none_handling: TokenStream,
+) -> TokenStream {
+    if arg_attr.list.is_some() {
+        quote_spanned! { span=>
+            let #extracted_name = args.map(::perlmod::Value::from);
+        }
+    } else {
+        quote_spanned! { span=>
+            let #extracted_name: ::perlmod::Value = match args.next() {
+                Some(arg) => ::perlmod::Value::from(arg),
+                None => #none_handling
+            };
         }
     }
+}
 
-    fn deserialized_argument_code(
-        &self,
-        span: Span,
-        arg_type: &syn::Type,
-        deserialized_name: &Ident,
-        extracted_name: Ident,
-    ) -> TokenStream {
-        if self.raw {
-            quote_spanned! { span=>
-                let #deserialized_name = #extracted_name;
-            }
-        } else if self.try_from_ref {
-            quote_spanned! { span=>
-                let #deserialized_name: #arg_type =
-                    match ::std::convert::TryFrom::try_from(&#extracted_name) {
-                        Ok(arg) => arg,
-                        Err(err) => {
-                            return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
-                                .into_mortal()
-                                .into_raw());
-                        }
-                    };
-            }
-        } else if self.list.is_some() {
-            quote_spanned! { span=>
-                let #deserialized_name = {
-                    let _guard = ::perlmod::__private__::InParameterDeserialization::guard();
-                    match <#arg_type as ::perlmod::__private__::serde::Deserialize>::deserialize(
-                        ::perlmod::__private__::serde::de::value::SeqDeserializer::new(
-                            #extracted_name
-                        )
-                    ) {
-                        Ok(arg) => arg,
-                        Err(err) => {
-                            return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
-                                .into_mortal()
-                                .into_raw());
-                        }
+fn deserialized_argument_code(
+    arg_attr: &ArgumentAttrs,
+    span: Span,
+    arg_type: &syn::Type,
+    deserialized_name: &Ident,
+    extracted_name: Ident,
+) -> TokenStream {
+    if arg_attr.raw {
+        quote_spanned! { span=>
+            let #deserialized_name = #extracted_name;
+        }
+    } else if arg_attr.try_from_ref {
+        quote_spanned! { span=>
+            let #deserialized_name: #arg_type =
+                match ::std::convert::TryFrom::try_from(&#extracted_name) {
+                    Ok(arg) => arg,
+                    Err(err) => {
+                        return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+                            .into_mortal()
+                            .into_raw());
+                    }
+                };
+        }
+    } else if arg_attr.list.is_some() {
+        quote_spanned! { span=>
+            let #deserialized_name = {
+                let _guard = ::perlmod::__private__::InParameterDeserialization::guard();
+                match <#arg_type as ::perlmod::__private__::serde::Deserialize>::deserialize(
+                    ::perlmod::__private__::serde::de::value::SeqDeserializer::new(
+                        #extracted_name
+                    )
+                ) {
+                    Ok(arg) => arg,
+                    Err(err) => {
+                        return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+                            .into_mortal()
+                            .into_raw());
+                    }
+                }
+            };
+        }
+    } else {
+        quote_spanned! { span=>
+            let #deserialized_name: #arg_type =
+                match ::perlmod::from_ref_value(&#extracted_name) {
+                    Ok(data) => data,
+                    Err(err) => {
+                        return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+                            .into_mortal()
+                            .into_raw());
                     }
                 };
-            }
-        } else {
-            quote_spanned! { span=>
-                let #deserialized_name: #arg_type =
-                    match ::perlmod::from_ref_value(&#extracted_name) {
-                        Ok(data) => data,
-                        Err(err) => {
-                            return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
-                                .into_mortal()
-                                .into_raw());
-                        }
-                    };
-            }
         }
     }
 }
@@ -278,13 +278,15 @@ pub fn handle_function(
             }
         };
 
-        extract_arguments.extend(argument_attrs.extract_argument_code(
+        extract_arguments.extend(extract_argument_code(
+            &argument_attrs,
             span,
             &extracted_name,
             none_handling,
         ));
 
-        deserialized_arguments.extend(argument_attrs.deserialized_argument_code(
+        deserialized_arguments.extend(deserialized_argument_code(
+            &argument_attrs,
             span,
             arg_type,
             &deserialized_name,
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH perlmod v2 2/5] macro: function: make argument attribute handling more extendable
  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 ` 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
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
  To: pve-devel

Introduce a new private `ArgumentAttrType` enum for tracking whether
an argument attribute is `#[raw]`, `#[list]`, etc. and replace the
flags inside `ArgumentAttrs` with it.

Move the reference to `syn::PatType` into `ArgumentAttrs` and make
`ArgumentAttrs` take a corresponding lifetime as parameter.

Replace the existing machinery that has the `for_input()` function as
entrypoint with the new `new_from_fn_arg()` function, which makes use
of the new fields in `ArgumentAttrs`. Replicate the previous logic
inside `new_from_fn_arg()`, but laid out more explicitly -- now there
is no need to bounce between a couple function calls when reading the
code.

Rename `ArgumentAttrs` to `ArgumentAttr`, since we never return more
than one anyway, and all argument attributes are mutually exclusive
anyway.

Finally, adapt the remaining code in `handle_function()`,
`extract_argument_code()`, and `deserialized_argument_code()`
correspondingly by making it use the new struct fields.

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

diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index 9d4a4af..3ce0a17 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -15,112 +15,159 @@ pub struct XSub {
     pub prototype: Option<String>,
 }
 
-#[derive(Default)]
-struct ArgumentAttrs {
-    /// This is the `CV` pointer.
-    cv: Option<Span>,
-
+enum ArgumentAttrType {
     /// Skip the deserializer for this argument.
-    raw: bool,
+    Raw,
 
     /// Call `TryFrom<&Value>::try_from` for this argument instead of deserializing it.
-    try_from_ref: bool,
+    TryFromRef,
+
+    /// This is the `CV` pointer.
+    CVPtr(Span),
 
     /// Slurp the remaining arguments (like an `@rest` at the end).
     /// This requires the parameter to implement `FromIterator<T: Deserialize>`.
-    list: Option<Span>,
+    TrailingList(Span),
 }
 
-impl ArgumentAttrs {
-    fn handle_path(&mut self, path: &syn::Path) -> bool {
+impl ArgumentAttrType {
+    fn from_attr(attr: &syn::Attribute) -> Option<Self> {
+        Self::from_path(attr.path())
+    }
+
+    fn from_path(path: &syn::Path) -> Option<Self> {
         if path.is_ident("raw") {
-            self.raw = true;
-        } else if path.is_ident("try_from_ref") {
-            self.try_from_ref = true;
-        } else if path.is_ident("cv") {
-            self.cv = Some(path.span());
-        } else if path.is_ident("list") {
-            self.list = Some(path.span());
-        } else {
-            return false;
+            return Some(Self::Raw);
         }
 
-        true
+        if path.is_ident("try_from_ref") {
+            return Some(Self::TryFromRef);
+        }
+
+        if path.is_ident("cv") {
+            return Some(Self::CVPtr(path.span()));
+        }
+
+        if path.is_ident("list") {
+            return Some(Self::TrailingList(path.span()));
+        }
+
+        None
     }
 
-    fn handle_attr(&mut self, attr: &syn::Attribute) -> bool {
-        if self.handle_path(attr.path()) {
-            if !matches!(attr.meta, Meta::Path(_)) {
-                error!(&attr.meta => "attribute does not take any value or parameter");
-            }
-            true
-        } else {
-            false
+    fn is_same_variant_as(&self, other: &Self) -> bool {
+        core::mem::discriminant(self) == core::mem::discriminant(other)
+    }
+
+    fn as_str(&self) -> &'static str {
+        match self {
+            Self::Raw => "raw",
+            Self::TryFromRef => "try_from_ref",
+            Self::CVPtr(_) => "cv",
+            Self::TrailingList(_) => "list",
         }
     }
+}
 
-    fn validate(&self, span: Span) -> Result<(), Error> {
-        if self.raw as usize
-            + self.try_from_ref as usize
-            + self.cv.is_some() as usize
-            + self.list.is_some() as usize
-            > 1
-        {
-            bail!(
-                span,
-                "`raw` and `try_from_ref`, `cv` and `list` attributes are mutually exclusive"
-            );
-        }
-        Ok(())
-    }
-
-    fn for_input(arg: &mut syn::FnArg) -> Result<(Self, &syn::PatType), Error> {
-        let mut this = Self::default();
+struct ArgumentAttr<'s> {
+    attr_type: Option<ArgumentAttrType>,
+    pat_type: &'s syn::PatType,
+}
 
+impl<'s> ArgumentAttr<'s> {
+    fn new_from_fn_arg(arg: &'s mut syn::FnArg) -> Result<Self, Error> {
         match arg {
-            syn::FnArg::Receiver(_) => bail!(arg => "cannot export self-taking methods as xsubs"),
-            syn::FnArg::Typed(pt) => {
-                pt.attrs.retain(|attr| !this.handle_attr(attr));
-                this.validate(pt.span())?;
-                Ok((this, &*pt))
+            syn::FnArg::Receiver(_) => {
+                bail!(arg => "cannot export self-taking methods as xsubs");
+            }
+            syn::FnArg::Typed(pat_type) => {
+                let mut attr_type: Option<ArgumentAttrType> = None;
+                let mut has_err = false;
+
+                let span = pat_type.span();
+
+                pat_type.attrs.retain(|attr| {
+                    let Some(got_attr_type) = ArgumentAttrType::from_attr(attr) else {
+                        // Attribute has no matching attribute type => retain it
+                        return true;
+                    };
+
+                    if !matches!(attr.meta, Meta::Path(_)) {
+                        error!(&attr.meta => "attribute does not take any value or parameter");
+                        has_err = true;
+                        return false;
+                    }
+
+                    let Some(existing_attr_type) = attr_type.as_ref() else {
+                        // No existing attribute type => assign and don't retain
+                        attr_type = Some(got_attr_type);
+                        return false;
+                    };
+
+                    if existing_attr_type.is_same_variant_as(&got_attr_type) {
+                        let attr_str = existing_attr_type.as_str();
+                        error!(span, "duplicate attribute '{attr_str}'");
+                        has_err = true;
+                        return false;
+                    }
+
+                    // At this point we have two differing attributes
+                    error!(
+                        span,
+                        "`raw`, `try_from_ref`, `cv`, and `list` attributes are mutually exclusive"
+                    );
+                    has_err = true;
+                    false
+                });
+
+                if has_err {
+                    bail!(span, "failed to determine argument attribute");
+                }
+
+                Ok(Self {
+                    attr_type,
+                    pat_type,
+                })
             }
         }
     }
 }
 
 fn extract_argument_code(
-    arg_attr: &ArgumentAttrs,
+    arg_attr: &ArgumentAttr,
     span: Span,
     extracted_name: &Ident,
     none_handling: TokenStream,
 ) -> TokenStream {
-    if arg_attr.list.is_some() {
-        quote_spanned! { span=>
-            let #extracted_name = args.map(::perlmod::Value::from);
+    match arg_attr.attr_type {
+        Some(ArgumentAttrType::TrailingList(_)) => {
+            quote_spanned! { span=>
+                let #extracted_name = args.map(::perlmod::Value::from);
+            }
         }
-    } else {
-        quote_spanned! { span=>
-            let #extracted_name: ::perlmod::Value = match args.next() {
-                Some(arg) => ::perlmod::Value::from(arg),
-                None => #none_handling
-            };
+        _ => {
+            quote_spanned! { span=>
+                let #extracted_name: ::perlmod::Value = match args.next() {
+                    Some(arg) => ::perlmod::Value::from(arg),
+                    None => #none_handling
+                };
+            }
         }
     }
 }
 
 fn deserialized_argument_code(
-    arg_attr: &ArgumentAttrs,
+    arg_attr: &ArgumentAttr,
     span: Span,
     arg_type: &syn::Type,
     deserialized_name: &Ident,
     extracted_name: Ident,
 ) -> TokenStream {
-    if arg_attr.raw {
-        quote_spanned! { span=>
+    match &arg_attr.attr_type {
+        Some(ArgumentAttrType::Raw) => quote_spanned! { span=>
             let #deserialized_name = #extracted_name;
-        }
-    } else if arg_attr.try_from_ref {
-        quote_spanned! { span=>
+        },
+        Some(ArgumentAttrType::TryFromRef) => quote_spanned! { span=>
             let #deserialized_name: #arg_type =
                 match ::std::convert::TryFrom::try_from(&#extracted_name) {
                     Ok(arg) => arg,
@@ -130,9 +177,8 @@ fn deserialized_argument_code(
                             .into_raw());
                     }
                 };
-        }
-    } else if arg_attr.list.is_some() {
-        quote_spanned! { span=>
+        },
+        Some(ArgumentAttrType::TrailingList(_)) => quote_spanned! { span=>
             let #deserialized_name = {
                 let _guard = ::perlmod::__private__::InParameterDeserialization::guard();
                 match <#arg_type as ::perlmod::__private__::serde::Deserialize>::deserialize(
@@ -148,9 +194,8 @@ fn deserialized_argument_code(
                     }
                 }
             };
-        }
-    } else {
-        quote_spanned! { span=>
+        },
+        Some(ArgumentAttrType::CVPtr(_)) | None => quote_spanned! { span=>
             let #deserialized_name: #arg_type =
                 match ::perlmod::from_ref_value(&#extracted_name) {
                     Ok(data) => data,
@@ -160,7 +205,7 @@ fn deserialized_argument_code(
                             .into_raw());
                     }
                 };
-        }
+        },
     }
 }
 
@@ -214,17 +259,17 @@ pub fn handle_function(
     let mut cv_arg_param = TokenStream::new();
     let mut had_list_param = false;
     for arg in &mut func.sig.inputs {
-        let (argument_attrs, pat_ty) = ArgumentAttrs::for_input(arg)?;
+        let arg_attr = ArgumentAttr::new_from_fn_arg(arg)?;
 
-        if had_list_param {
-            if let Some(span) = argument_attrs.list {
-                bail!(span, "only 1 #[list] parameter allowed");
+        if let Some(ArgumentAttrType::TrailingList(list_span)) = arg_attr.attr_type {
+            if had_list_param {
+                bail!(list_span, "only 1 #[list] parameter allowed");
             }
-        } else {
-            had_list_param = argument_attrs.list.is_some();
+
+            had_list_param = true;
         }
 
-        let arg_name = match &*pat_ty.pat {
+        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");
@@ -234,12 +279,12 @@ pub fn handle_function(
                 }
                 &ident.ident
             }
-            _ => bail!(&pat_ty.pat => "xsub does not support this kind of parameter"),
+            _ => bail!(&arg_attr.pat_type.pat => "xsub does not support this kind of parameter"),
         };
 
-        let arg_type = &*pat_ty.ty;
+        let arg_type = &*arg_attr.pat_type.ty;
 
-        if let Some(cv_span) = argument_attrs.cv {
+        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");
             }
@@ -264,7 +309,7 @@ pub fn handle_function(
         let none_handling = if is_option_type(arg_type).is_some() {
             trailing_options += 1;
             quote_spanned! { span=> ::perlmod::Value::new_undef(), }
-        } else if argument_attrs.list.is_some() {
+        } else if matches!(arg_attr.attr_type, Some(ArgumentAttrType::TrailingList(_))) {
             TokenStream::new()
         } else {
             // only count the trailing options;
@@ -279,14 +324,14 @@ pub fn handle_function(
         };
 
         extract_arguments.extend(extract_argument_code(
-            &argument_attrs,
+            &arg_attr,
             span,
             &extracted_name,
             none_handling,
         ));
 
         deserialized_arguments.extend(deserialized_argument_code(
-            &argument_attrs,
+            &arg_attr,
             span,
             arg_type,
             &deserialized_name,
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH perlmod v2 3/5] macro: function: explicitly pass identifier for generated arg iter
  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 ` Max R. Carrara
  2026-07-24 14:49 ` [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct Max R. Carrara
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
  To: pve-devel

The `args` variable that we define in generated code is used in
multiple places, making them implicitly depend on each other.

Make this explicit instead by defining the identifier as a
`syn::Ident` and passing it to / interpolating it into the respective
places.

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

diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index 3ce0a17..3ca7bd0 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -136,18 +136,19 @@ impl<'s> ArgumentAttr<'s> {
 fn extract_argument_code(
     arg_attr: &ArgumentAttr,
     span: Span,
+    arguments_name: &Ident,
     extracted_name: &Ident,
     none_handling: TokenStream,
 ) -> TokenStream {
     match arg_attr.attr_type {
         Some(ArgumentAttrType::TrailingList(_)) => {
             quote_spanned! { span=>
-                let #extracted_name = args.map(::perlmod::Value::from);
+                let #extracted_name = #arguments_name.map(::perlmod::Value::from);
             }
         }
         _ => {
             quote_spanned! { span=>
-                let #extracted_name: ::perlmod::Value = match args.next() {
+                let #extracted_name: ::perlmod::Value = match #arguments_name.next() {
                     Some(arg) => ::perlmod::Value::from(arg),
                     None => #none_handling
                 };
@@ -252,6 +253,8 @@ pub fn handle_function(
         });
     let impl_xs_name = Ident::new(&format!("impl_xs_{name}"), name.span());
 
+    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();
@@ -326,6 +329,7 @@ pub fn handle_function(
         extract_arguments.extend(extract_argument_code(
             &arg_attr,
             span,
+            &arguments_name,
             &extracted_name,
             none_handling,
         ));
@@ -377,7 +381,7 @@ pub fn handle_function(
         );
 
         quote_spanned! { span=>
-            if args.next().is_some() {
+            if #arguments_name.next().is_some() {
                 return Err(::perlmod::Value::new_string(#too_many_args_error)
                     .into_mortal()
                     .into_raw());
@@ -417,8 +421,8 @@ pub fn handle_function(
             #visibility_action
 
             let argmark = unsafe { ::perlmod::ffi::pop_arg_mark() };
-            let mut args = argmark.iter();
-            { let _ = &mut args; }
+            let mut #arguments_name = argmark.iter();
+            { let _ = &mut #arguments_name; }
 
             #extract_arguments
 
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct
  2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
                   ` (2 preceding siblings ...)
  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
  2026-07-30 10:23   ` Wolfgang Bumiller
  2026-07-24 14:49 ` [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute Max R. Carrara
                   ` (2 subsequent siblings)
  6 siblings, 1 reply; 11+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
  To: pve-devel

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





^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute
  2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
                   ` (3 preceding siblings ...)
  2026-07-24 14:49 ` [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct Max R. Carrara
@ 2026-07-24 14:49 ` Max R. Carrara
  2026-07-30 10:23   ` Wolfgang Bumiller
  2026-07-30 10:26 ` partially-applied: [PATCH perlmod v2 0/5] perlmod: " Wolfgang Bumiller
  2026-07-30 11:12 ` superseded: " Max R. Carrara
  6 siblings, 1 reply; 11+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
  To: pve-devel

Like `#[list]`, `#[hash]` allows us to support subs for which any
number of additional key-value pairs may be passed, which is the same
as having an `;%` at the end of the Perl prototype.

The `ParamsIter` type is added as a helper for pairwise iteration that
makes use of the fact that `ffi::StackIter` is now an
`ExactSizeIterator`, returning an error if the number of values on the
stack is odd on creation. It is then directly passed on to
serde's `MapDeserializer`, mirroring how `#[list]` works with
`SeqDeserializer`.

That way we can support additional Perl APIs without new parameters
breaking the Rust side of the API, just like we can with `#[list]`.

Also document the `#[hash]` parameter and add corresponding tests
along the way.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 perlmod-macro/src/function.rs | 69 +++++++++++++++++++++++++++++++----
 perlmod/src/de.rs             | 44 ++++++++++++++++++++++
 perlmod/src/lib.rs            |  5 +++
 testlib-tests/01-hello.t      | 46 ++++++++++++++++++++++-
 testlib/src/lib.rs            | 24 +++++++++++-
 5 files changed, 178 insertions(+), 10 deletions(-)

diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index f5415c5..aa48cc9 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -28,6 +28,11 @@ enum ArgumentAttrType {
     /// Slurp the remaining arguments (like an `@rest` at the end).
     /// This requires the parameter to implement `FromIterator<T: Deserialize>`.
     TrailingList(Span),
+
+    /// Slurp the remaining arguments (like a `%hash` at the end).
+    /// This requires the parameter to implement `FromIterator<(K, V)> where K: Deserialize, V:
+    /// Deserialize`.
+    TrailingHash(Span),
 }
 
 impl ArgumentAttrType {
@@ -52,6 +57,10 @@ impl ArgumentAttrType {
             return Some(Self::TrailingList(path.span()));
         }
 
+        if path.is_ident("hash") {
+            return Some(Self::TrailingHash(path.span()));
+        }
+
         None
     }
 
@@ -65,6 +74,7 @@ impl ArgumentAttrType {
             Self::TryFromRef => "try_from_ref",
             Self::CVPtr(_) => "cv",
             Self::TrailingList(_) => "list",
+            Self::TrailingHash(_) => "hash",
         }
     }
 }
@@ -114,7 +124,7 @@ impl<'s> ArgumentAttr<'s> {
                     // At this point we have two differing attributes
                     error!(
                         span,
-                        "`raw`, `try_from_ref`, `cv`, and `list` attributes are mutually exclusive"
+                        "`raw`, `try_from_ref`, `cv`, `list`, and `hash` attributes are mutually exclusive"
                     );
                     has_err = true;
                     false
@@ -141,7 +151,7 @@ fn extract_argument_code(
     none_handling: TokenStream,
 ) -> TokenStream {
     match arg_attr.attr_type {
-        Some(ArgumentAttrType::TrailingList(_)) => {
+        Some(ArgumentAttrType::TrailingList(_) | ArgumentAttrType::TrailingHash(_)) => {
             quote_spanned! { span=>
                 let #extracted_name = #arguments_name.map(::perlmod::Value::from);
             }
@@ -196,6 +206,32 @@ fn deserialized_argument_code(
                 }
             };
         },
+        Some(ArgumentAttrType::TrailingHash(_)) => quote_spanned! { span=>
+            let #deserialized_name = {
+                let _guard = ::perlmod::__private__::InParameterDeserialization::guard();
+                match  ::perlmod::de::ParamsIter::new(#extracted_name) {
+                    Ok(params_iter) => {
+                        match <#arg_type as ::perlmod::__private__::serde::Deserialize>::deserialize(
+                            ::perlmod::__private__::serde::de::value::MapDeserializer::new(
+                                params_iter
+                            )
+                        ) {
+                            Ok(map) => map,
+                            Err(err) => {
+                                return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+                                    .into_mortal()
+                                    .into_raw());
+                            }
+                        }
+                    },
+                    Err(err) => {
+                        return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+                            .into_mortal()
+                            .into_raw());
+                    }
+                }
+            };
+        },
         Some(ArgumentAttrType::CVPtr(_)) | None => quote_spanned! { span=>
             let #deserialized_name: #arg_type =
                 match ::perlmod::from_ref_value(&#extracted_name) {
@@ -212,12 +248,22 @@ fn deserialized_argument_code(
 
 enum TrailingType {
     List,
+    Hash,
 }
 
 impl TrailingType {
     fn as_str(&self) -> &'static str {
         match self {
             Self::List => "list",
+            Self::Hash => "hash",
+        }
+    }
+
+    fn from_attr_type(attr_type: &ArgumentAttrType) -> Self {
+        match attr_type {
+            ArgumentAttrType::TrailingList(_) => Self::List,
+            ArgumentAttrType::TrailingHash(_) => Self::Hash,
+            _ => unreachable!(),
         }
     }
 }
@@ -272,10 +318,12 @@ impl FnInputs {
             };
 
             if let Some(ref attr_type) = arg_attr.attr_type {
+                use ArgumentAttrType as AT;
+
                 match attr_type {
-                    ArgumentAttrType::Raw => {}
-                    ArgumentAttrType::TryFromRef => {}
-                    ArgumentAttrType::CVPtr(cv_span) => {
+                    AT::Raw => {}
+                    AT::TryFromRef => {}
+                    AT::CVPtr(cv_span) => {
                         if !state.cv_arg_param.is_empty() {
                             bail!(*cv_span, "only 1 'cv' parameter allowed");
                         }
@@ -293,7 +341,7 @@ impl FnInputs {
 
                         continue;
                     }
-                    ArgumentAttrType::TrailingList(trailing_span) => {
+                    AT::TrailingList(trailing_span) | AT::TrailingHash(trailing_span) => {
                         if let Some(trailing_type) = state.trailing_type {
                             bail!(
                                 *trailing_span,
@@ -302,7 +350,7 @@ impl FnInputs {
                             );
                         }
 
-                        state.trailing_type = Some(TrailingType::List);
+                        state.trailing_type = Some(TrailingType::from_attr_type(attr_type));
                     }
                 }
             }
@@ -322,7 +370,10 @@ impl FnInputs {
                     break 'handling quote_spanned! { span=> ::perlmod::Value::new_undef(), };
                 }
 
-                if matches!(arg_attr.attr_type, Some(ArgumentAttrType::TrailingList(_))) {
+                if matches!(
+                    arg_attr.attr_type,
+                    Some(ArgumentAttrType::TrailingList(_) | ArgumentAttrType::TrailingHash(_))
+                ) {
                     break 'handling TokenStream::new();
                 }
 
@@ -555,6 +606,7 @@ fn gen_prototype(total_arg_count: usize, inputs: &FnInputs) -> String {
 
             match ty {
                 Some(TrailingType::List) => proto.push('@'),
+                Some(TrailingType::Hash) => proto.push('%'),
                 None => {}
             }
 
@@ -563,6 +615,7 @@ fn gen_prototype(total_arg_count: usize, inputs: &FnInputs) -> String {
         (0, ty) => {
             match ty {
                 Some(TrailingType::List) => proto.push_str(";@"),
+                Some(TrailingType::Hash) => proto.push_str(";%"),
                 None => {}
             }
 
diff --git a/perlmod/src/de.rs b/perlmod/src/de.rs
index cf1601f..1355d0c 100644
--- a/perlmod/src/de.rs
+++ b/perlmod/src/de.rs
@@ -782,3 +782,47 @@ impl<'de> MapAccess<'de> for RawDeserializer<'_> {
         }
     }
 }
+
+pub struct ParamsIter<I> {
+    inner: I,
+}
+
+impl<I> ParamsIter<I>
+where
+    I: Iterator<Item = Value> + ExactSizeIterator,
+{
+    pub fn new<T>(into_iter: T) -> Result<Self, Error>
+    where
+        T: IntoIterator<Item = I::Item, IntoIter = I>,
+    {
+        let iter = into_iter.into_iter();
+
+        if iter.len() % 2 != 0 {
+            Err(Error::new(
+                "odd number of elements for parameter hash - must be even",
+            ))
+        } else {
+            Ok(Self { inner: iter })
+        }
+    }
+}
+
+impl<I> Iterator for ParamsIter<I>
+where
+    I: Iterator<Item = Value> + ExactSizeIterator,
+{
+    type Item = (Value, Value);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(key) = self.inner.next() {
+            let value = self
+                .inner
+                .next()
+                .expect("expected value for key - odd number of elements in params iterator");
+
+            Some((key, value))
+        } else {
+            None
+        }
+    }
+}
diff --git a/perlmod/src/lib.rs b/perlmod/src/lib.rs
index 83b9d8b..2dd1eb2 100644
--- a/perlmod/src/lib.rs
+++ b/perlmod/src/lib.rs
@@ -119,6 +119,11 @@ pub use perlmod_macro::package;
 ///   `serde::de::value::SeqDeserializer`.) This causes the prototype to end with `;@` (behaves
 ///   correctly with `Option` parameters in front of it).
 ///
+/// * `#[hash]`: Like `#[list]`, but instead of deserializing from a sequence, the final parameter
+///   needs to be able to deserialize from a map. (Technically just adapts Perl's argument stack
+///   into a `serde::de::value::MapDeserializer`.) This causes the prototype to end with `;%`
+///   (behaves correctly with `Option` parameters in front of it).
+///
 /// For an example on making blessed objects, see [`Value::bless_box`](Value::bless_box()).
 pub use perlmod_macro::export;
 
diff --git a/testlib-tests/01-hello.t b/testlib-tests/01-hello.t
index 535e5fb..9770525 100644
--- a/testlib-tests/01-hello.t
+++ b/testlib-tests/01-hello.t
@@ -1,6 +1,6 @@
 use v5.36;
 
-use Test::More tests => 16;
+use Test::More tests => 24;
 
 use TestLib::Hello;
 
@@ -63,3 +63,47 @@ is(
     'final option unset, empty list',
 );
 is(TestLib::Hello::sum_list(50, 51, 52), 153, 'pass a deserialized list');
+
+is(
+    TestLib::Hello::trailing_hash(60, 61 => 62),
+    'first=60, rest is {61: 62}',
+    'collecting 1 trailing key-value pair',
+);
+is(
+    TestLib::Hello::trailing_hash_ordered(63, 64 => 65, 66 => 67),
+    'first=63, rest is {64: 65, 66: 67}',
+    'collecting 2 trailing key-value pairs',
+);
+is(
+    TestLib::Hello::trailing_hash(67),
+    'first=67, rest is {}',
+    'collecting 0 trailing key-value pairs',
+);
+
+is(
+    eval { TestLib::Hello::trailing_hash(68, 69) } // $@,
+    "error: odd number of elements for parameter hash - must be even\n",
+    'failing to collect odd number of trailing hash parameters',
+);
+
+is(
+    TestLib::Hello::trailing_hash_and_options(80),
+    'first=80, second=None, rest is {}',
+    'final option unset, collecting 0 trailing key-value pairs',
+);
+is(
+    TestLib::Hello::trailing_hash_and_options(81, 82),
+    'first=81, second=Some(82), rest is {}',
+    'final option set, collecting 0 trailing key-value pairs',
+);
+is(
+    TestLib::Hello::trailing_hash_and_options(81, 82, 83 => 84),
+    'first=81, second=Some(82), rest is {83: 84}',
+    'final option set, collecting 1 trailing key-value pairs',
+);
+
+is(
+    eval { TestLib::Hello::trailing_hash_and_options(85, 86, 87) } // $@,
+    "error: odd number of elements for parameter hash - must be even\n",
+    'final option set, collecting 1 trailing key-value pairs',
+);
diff --git a/testlib/src/lib.rs b/testlib/src/lib.rs
index ac3bfde..81879cc 100644
--- a/testlib/src/lib.rs
+++ b/testlib/src/lib.rs
@@ -12,7 +12,10 @@ mod main_lib {}
 
 #[perlmod::package(name = "TestLib::Hello", lib = "testlib", boot = "loaded")]
 mod export {
-    use std::sync::atomic::{AtomicBool, Ordering};
+    use std::{
+        collections::{BTreeMap, HashMap},
+        sync::atomic::{AtomicBool, Ordering},
+    };
 
     use anyhow::{Error, bail};
     use serde::{Deserialize, Serialize};
@@ -137,4 +140,23 @@ mod export {
     fn sum_list(#[list] rest: Vec<u32>) -> u32 {
         rest.into_iter().sum()
     }
+
+    #[export]
+    fn trailing_hash(first: u32, #[hash] rest: HashMap<u32, u32>) -> String {
+        format!("first={first}, rest is {rest:?}")
+    }
+
+    #[export]
+    fn trailing_hash_ordered(first: u32, #[hash] rest: BTreeMap<u32, u32>) -> String {
+        format!("first={first}, rest is {rest:?}")
+    }
+
+    #[export]
+    fn trailing_hash_and_options(
+        first: u32,
+        second: Option<u32>,
+        #[hash] rest: HashMap<u32, u32>,
+    ) -> String {
+        format!("first={first}, second={second:?}, rest is {rest:?}")
+    }
 }
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct
  2026-07-24 14:49 ` [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct Max R. Carrara
@ 2026-07-30 10:23   ` Wolfgang Bumiller
  2026-07-30 10:56     ` Max R. Carrara
  0 siblings, 1 reply; 11+ messages in thread
From: Wolfgang Bumiller @ 2026-07-30 10:23 UTC (permalink / raw)
  To: Max R. Carrara; +Cc: pve-devel

On Fri, 24 Jul 2026 16:49:06 +0200, Max R. Carrara <m.carrara@proxmox.com> wrote:
> 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(
> [ ... skip 44 lines ... ]
> +            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();

This binding is unnecessary - let's drop the enclosing block and
indentation and match directly…

> +                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"),

… ↑ with `s/_/pattern/` here.

> @@ -451,36 +527,48 @@ pub fn handle_function(
> [ ... skip 30 lines ... ]
> -        proto.push(';');
> -        for _ in 0..trailing_options {
> -            proto.push('$');
> +
> +    match (trailing_options, trailing_type) {
> +        (1.., ty) => {

Not too happy about this - the `if` previously was sufficient, and the
`match` now doesn't do any matching on `trailing_type` - it just rebinds
it to `ty`, so just more indentation for no gain?

-- 





^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute
  2026-07-24 14:49 ` [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute Max R. Carrara
@ 2026-07-30 10:23   ` Wolfgang Bumiller
  0 siblings, 0 replies; 11+ messages in thread
From: Wolfgang Bumiller @ 2026-07-30 10:23 UTC (permalink / raw)
  To: Max R. Carrara; +Cc: pve-devel

On Fri, 24 Jul 2026 16:49:07 +0200, Max R. Carrara <m.carrara@proxmox.com> wrote:
> Like `#[list]`, `#[hash]` allows us to support subs for which any
> number of additional key-value pairs may be passed, which is the same
> as having an `;%` at the end of the Perl prototype.
> 
> The `ParamsIter` type is added as a helper for pairwise iteration that
> makes use of the fact that `ffi::StackIter` is now an
> `ExactSizeIterator`, returning an error if the number of values on the
> stack is odd on creation. It is then directly passed on to
> serde's `MapDeserializer`, mirroring how `#[list]` works with
> `SeqDeserializer`.
> 
> [...]

Acked-by: Wolfgang Bumiller <w.bumiller@proxmox.com>

-- 





^ permalink raw reply	[flat|nested] 11+ messages in thread

* partially-applied: [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute
  2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
                   ` (4 preceding siblings ...)
  2026-07-24 14:49 ` [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute Max R. Carrara
@ 2026-07-30 10:26 ` Wolfgang Bumiller
  2026-07-30 11:12 ` superseded: " Max R. Carrara
  6 siblings, 0 replies; 11+ messages in thread
From: Wolfgang Bumiller @ 2026-07-30 10:26 UTC (permalink / raw)
  To: Max R. Carrara; +Cc: pve-devel

applied 1..3, thanks




^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct
  2026-07-30 10:23   ` Wolfgang Bumiller
@ 2026-07-30 10:56     ` Max R. Carrara
  0 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2026-07-30 10:56 UTC (permalink / raw)
  To: Wolfgang Bumiller; +Cc: pve-devel

On Thu Jul 30, 2026 at 12:23 PM CEST, Wolfgang Bumiller wrote:
> On Fri, 24 Jul 2026 16:49:06 +0200, Max R. Carrara <m.carrara@proxmox.com> wrote:
> > 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(
> > [ ... skip 44 lines ... ]
> > +            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();
>
> This binding is unnecessary - let's drop the enclosing block and
> indentation and match directly…

Looking at this again, yeah I agree -- will fix in v3. Thanks!

>
> > +                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"),
>
> … ↑ with `s/_/pattern/` here.
>
> > @@ -451,36 +527,48 @@ pub fn handle_function(
> > [ ... skip 30 lines ... ]
> > -        proto.push(';');
> > -        for _ in 0..trailing_options {
> > -            proto.push('$');
> > +
> > +    match (trailing_options, trailing_type) {
> > +        (1.., ty) => {
>
> Not too happy about this - the `if` previously was sufficient, and the
> `match` now doesn't do any matching on `trailing_type` - it just rebinds
> it to `ty`, so just more indentation for no gain?

Same as above here; looks odd now that I look at it again. Will also fix
in v3, thanks!





^ permalink raw reply	[flat|nested] 11+ messages in thread

* superseded: [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute
  2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
                   ` (5 preceding siblings ...)
  2026-07-30 10:26 ` partially-applied: [PATCH perlmod v2 0/5] perlmod: " Wolfgang Bumiller
@ 2026-07-30 11:12 ` Max R. Carrara
  6 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2026-07-30 11:12 UTC (permalink / raw)
  To: Max R. Carrara, pve-devel

On Fri Jul 24, 2026 at 4:49 PM CEST, Max R. Carrara wrote:
> perlmod: add #[hash] parameter attribute - v2
> =============================================
>
> This series implements a `#[hash]` parameter similar to the existing
> `#[list]` one, allowing us to create APIs like
> `foo("arg", key => "value")` directly from Rust.
>
> [...]

Superseded-by: https://lore.proxmox.com/pve-devel/20260730111101.222209-1-m.carrara@proxmox.com/




^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-07-30 11:12 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct Max R. Carrara
2026-07-30 10:23   ` Wolfgang Bumiller
2026-07-30 10:56     ` Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute Max R. Carrara
2026-07-30 10:23   ` Wolfgang Bumiller
2026-07-30 10:26 ` partially-applied: [PATCH perlmod v2 0/5] perlmod: " Wolfgang Bumiller
2026-07-30 11:12 ` superseded: " Max R. Carrara

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal