Skip to main content

miniextendr_macros/
list_macro.rs

1//! Parser for the `list!` macro.
2//!
3//! A simple macro for constructing R lists from Rust values.
4//!
5//! ```ignore
6//! // Named entries (like R's list())
7//! list!(
8//!     alpha = 1,
9//!     beta = "hello",
10//!     gamma = vec![1, 2, 3],
11//! )
12//!
13//! // Unnamed entries
14//! list!(1, "hello", vec![1, 2, 3])
15//!
16//! // Mixed (unnamed entries get empty string names)
17//! list!(alpha = 1, 2, beta = "hello")
18//! ```
19
20use proc_macro2::TokenStream;
21use quote::quote;
22use syn::parse::{Parse, ParseStream};
23use syn::punctuated::Punctuated;
24use syn::{Expr, Ident, LitStr, Token};
25
26/// Parsed `list!` macro input containing zero or more entries.
27pub struct ListInput {
28    /// The list entries, which may be named, unnamed, or a mix.
29    pub entries: Vec<ListEntry>,
30}
31
32/// A single entry in the list.
33pub struct ListEntry {
34    /// The name (None for unnamed entries).
35    pub name: Option<ListName>,
36    /// The value expression.
37    pub value: Expr,
38}
39
40/// Name for a list entry -- either a Rust identifier or a string literal.
41///
42/// Identifiers are used for simple names (e.g., `alpha = 1`), while string
43/// literals allow names that are not valid Rust identifiers (e.g., `"my-name" = 1`).
44pub enum ListName {
45    /// A bare Rust identifier used as the entry name (e.g., `alpha`).
46    Ident(Ident),
47    /// A string literal used as the entry name (e.g., `"my-name"`).
48    Str(LitStr),
49}
50
51impl ListName {
52    /// Returns the name as a plain `String`, extracting the identifier text
53    /// or the string literal value.
54    fn to_string_value(&self) -> String {
55        match self {
56            ListName::Ident(ident) => ident.to_string(),
57            ListName::Str(lit) => lit.value(),
58        }
59    }
60}
61
62impl Parse for ListInput {
63    fn parse(input: ParseStream) -> syn::Result<Self> {
64        if input.is_empty() {
65            return Ok(ListInput {
66                entries: Vec::new(),
67            });
68        }
69
70        let entries_punct: Punctuated<ListEntry, Token![,]> = Punctuated::parse_terminated(input)?;
71        let entries: Vec<ListEntry> = entries_punct.into_iter().collect();
72
73        Ok(ListInput { entries })
74    }
75}
76
77impl Parse for ListEntry {
78    fn parse(input: ParseStream) -> syn::Result<Self> {
79        // Try to parse as `name = value` or `"name" = value` (like R's list())
80        // We need to look ahead to see if there's a `=`
81
82        // Check for identifier followed by =
83        if input.peek(Ident) && input.peek2(Token![=]) {
84            let name: Ident = input.parse()?;
85            input.parse::<Token![=]>()?;
86            let value: Expr = input.parse()?;
87            return Ok(ListEntry {
88                name: Some(ListName::Ident(name)),
89                value,
90            });
91        }
92
93        // Check for string literal followed by =
94        if input.peek(LitStr) && input.peek2(Token![=]) {
95            let name: LitStr = input.parse()?;
96            input.parse::<Token![=]>()?;
97            let value: Expr = input.parse()?;
98            return Ok(ListEntry {
99                name: Some(ListName::Str(name)),
100                value,
101            });
102        }
103
104        // Otherwise, parse as unnamed value
105        let value: Expr = input.parse()?;
106        Ok(ListEntry { name: None, value })
107    }
108}
109
110/// Expands a parsed `list!` invocation into a `List` constructor token stream.
111///
112/// For fully unnamed lists, generates `List::from_raw_values(...)`.
113/// For named or mixed lists, generates `List::from_raw_pairs(...)` where
114/// unnamed entries receive empty-string names.
115///
116/// Each entry value is converted via `IntoR::into_sexp()`.
117pub fn expand_list(input: ListInput) -> TokenStream {
118    if input.entries.is_empty() {
119        // Empty list
120        return quote! {
121            ::miniextendr_api::list::List::from_raw_values(::std::vec![])
122        };
123    }
124
125    // Check if all entries are unnamed
126    let all_unnamed = input.entries.iter().all(|e| e.name.is_none());
127
128    // Each entry's `into_sexp()` is wrapped in `__scope.protect_raw(...)` so
129    // earlier entries stay rooted across later allocations. Without this, the
130    // raw `vec![into_sexp(a), into_sexp(b), ...]` form leaves every prior SEXP
131    // unrooted across the next allocation — UAF under gctorture
132    // (reviews/2026-05-07-gctorture-audit.md).
133    if all_unnamed {
134        // Use from_raw_values for unnamed lists
135        let values: Vec<TokenStream> = input
136            .entries
137            .into_iter()
138            .map(|entry| {
139                let value = entry.value;
140                quote! {
141                    __scope.protect_raw(::miniextendr_api::into_r::IntoR::into_sexp(#value))
142                }
143            })
144            .collect();
145
146        quote! {
147            // SAFETY: list! is invoked from `#[miniextendr]` wrappers, which
148            // run on the R main thread. The scope unprotects on drop after
149            // the parent list has rooted its children via the write barrier.
150            unsafe {
151                let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
152                ::miniextendr_api::list::List::from_raw_values(::std::vec![#(#values),*])
153            }
154        }
155    } else {
156        // Use from_raw_pairs for named (or mixed) lists
157        let pairs: Vec<TokenStream> = input
158            .entries
159            .into_iter()
160            .map(|entry| {
161                let name = entry.name.map(|n| n.to_string_value()).unwrap_or_default(); // Empty string for unnamed
162                let value = entry.value;
163                quote! {
164                    (#name, __scope.protect_raw(::miniextendr_api::into_r::IntoR::into_sexp(#value)))
165                }
166            })
167            .collect();
168
169        quote! {
170            // SAFETY: see above.
171            unsafe {
172                let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
173                ::miniextendr_api::list::List::from_raw_pairs(::std::vec![#(#pairs),*])
174            }
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_parse_empty() {
185        let input: ListInput = syn::parse_quote!();
186        assert!(input.entries.is_empty());
187    }
188
189    #[test]
190    fn test_parse_unnamed() {
191        let input: ListInput = syn::parse_quote!(1, 2, 3);
192        assert_eq!(input.entries.len(), 3);
193        assert!(input.entries.iter().all(|e| e.name.is_none()));
194    }
195
196    #[test]
197    fn test_parse_named_ident() {
198        let input: ListInput = syn::parse_quote!(alpha = 1, beta = 2);
199        assert_eq!(input.entries.len(), 2);
200        match &input.entries[0].name {
201            Some(ListName::Ident(i)) => assert_eq!(i.to_string(), "alpha"),
202            _ => panic!("expected ident name"),
203        }
204    }
205
206    #[test]
207    fn test_parse_named_string() {
208        let input: ListInput = syn::parse_quote!("my-name" = 1);
209        assert_eq!(input.entries.len(), 1);
210        match &input.entries[0].name {
211            Some(ListName::Str(s)) => assert_eq!(s.value(), "my-name"),
212            _ => panic!("expected string name"),
213        }
214    }
215
216    #[test]
217    fn test_parse_mixed() {
218        let input: ListInput = syn::parse_quote!(alpha = 1, 2, beta = 3);
219        assert_eq!(input.entries.len(), 3);
220        assert!(input.entries[0].name.is_some());
221        assert!(input.entries[1].name.is_none());
222        assert!(input.entries[2].name.is_some());
223    }
224}