Skip to main content

miniextendr_macros/
type_inspect.rs

1//! Lightweight type-introspection helpers shared by parsing and codegen.
2
3/// Returns the `n`-th generic type argument from a path segment.
4pub(crate) fn nth_type_argument(seg: &syn::PathSegment, n: usize) -> Option<&syn::Type> {
5    if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
6        let mut count = 0;
7        for arg in ab.args.iter() {
8            if let syn::GenericArgument::Type(ty) = arg {
9                if count == n {
10                    return Some(ty);
11                }
12                count += 1;
13            }
14        }
15    }
16    None
17}
18
19/// Returns the first generic type argument from a path segment.
20pub(crate) fn first_type_argument(seg: &syn::PathSegment) -> Option<&syn::Type> {
21    nth_type_argument(seg, 0)
22}
23
24/// Returns the second generic type argument from a path segment.
25pub(crate) fn second_type_argument(seg: &syn::PathSegment) -> Option<&syn::Type> {
26    nth_type_argument(seg, 1)
27}
28
29/// Returns `true` if `ty` is syntactically `SEXP`.
30#[inline]
31pub(crate) fn is_sexp_type(ty: &syn::Type) -> bool {
32    matches!(ty, syn::Type::Path(p) if p
33        .path
34        .segments
35        .last()
36        .map(|s| s.ident == "SEXP")
37        .unwrap_or(false))
38}
39
40/// Framework type names that hold R memory and are `!Send` by design.
41///
42/// Used only for thread-strategy selection: values of these types can neither
43/// move into the worker closure nor cross back out of it (`run_on_worker`
44/// requires `Send`), so any function touching one stays on the main thread
45/// even under `worker-default`. Covers the raw `SEXP`, `AltrepSexp`, the
46/// zero-copy R-backed views (`RDVector`, `RDMatrix`, `RndVec`, `RndMat`,
47/// `ProtectedStrVec`), and the owned GC-rooted handles (`BuiltDataFrame`,
48/// `DataFrameShape`). Arbitrary user `!Send` types can't be detected
49/// syntactically — those need an explicit `no_worker`.
50const MAIN_THREAD_BOUND: &[&str] = &[
51    "SEXP",
52    "AltrepSexp",
53    "RDVector",
54    "RDMatrix",
55    "RndVec",
56    "RndMat",
57    "ProtectedStrVec",
58    "BuiltDataFrame",
59    "DataFrameShape",
60];
61
62/// Returns `true` if `ty` is an input type bound to the R main thread.
63///
64/// Checks only the outermost path segment: main-thread-bound inputs arrive
65/// bare (`x: SEXP`, `v: RDVector<f64>`), never nested inside containers.
66/// Return-type analysis keeps the narrower [`is_sexp_type`] and uses the
67/// recursive [`is_main_thread_bound_return`] for thread selection.
68#[inline]
69pub(crate) fn is_main_thread_bound_input(ty: &syn::Type) -> bool {
70    matches!(ty, syn::Type::Path(p) if p
71        .path
72        .segments
73        .last()
74        .map(|s| MAIN_THREAD_BOUND.contains(&s.ident.to_string().as_str()))
75        .unwrap_or(false))
76}
77
78/// Returns `true` if `ty` is (or contains) a main-thread-bound type anywhere
79/// in a return position — e.g. `BuiltDataFrame`, `Result<BuiltDataFrame,
80/// String>`, `Option<DataFrameShape>`, `Vec<BuiltDataFrame>`.
81///
82/// Unlike inputs, main-thread-bound returns routinely nest inside `Result` /
83/// `Option` / containers, so this walks the whole type tree. Under
84/// `worker-default` a function whose return type matches is forced onto the
85/// main thread: the value owns R memory (`!Send`) and cannot cross back from
86/// the worker (`run_on_worker` requires `T: Send`).
87pub(crate) fn is_main_thread_bound_return(ty: &syn::Type) -> bool {
88    match ty {
89        syn::Type::Path(p) => p.path.segments.last().is_some_and(|seg| {
90            if MAIN_THREAD_BOUND.contains(&seg.ident.to_string().as_str()) {
91                return true;
92            }
93            if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
94                return ab.args.iter().any(|arg| {
95                    matches!(arg, syn::GenericArgument::Type(t) if is_main_thread_bound_return(t))
96                });
97            }
98            false
99        }),
100        syn::Type::Reference(r) => is_main_thread_bound_return(&r.elem),
101        syn::Type::Paren(p) => is_main_thread_bound_return(&p.elem),
102        syn::Type::Group(g) => is_main_thread_bound_return(&g.elem),
103        syn::Type::Tuple(t) => t.elems.iter().any(is_main_thread_bound_return),
104        syn::Type::Array(a) => is_main_thread_bound_return(&a.elem),
105        syn::Type::Slice(s) => is_main_thread_bound_return(&s.elem),
106        _ => false,
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::is_main_thread_bound_return;
113
114    fn ty(s: &str) -> syn::Type {
115        syn::parse_str(s).unwrap()
116    }
117
118    #[test]
119    fn main_thread_bound_return_detects_nested_positions() {
120        // Bare and fully-qualified paths
121        assert!(is_main_thread_bound_return(&ty("BuiltDataFrame")));
122        assert!(is_main_thread_bound_return(&ty(
123            "miniextendr_api::dataframe::BuiltDataFrame"
124        )));
125        assert!(is_main_thread_bound_return(&ty("DataFrameShape")));
126        assert!(is_main_thread_bound_return(&ty("SEXP")));
127        // Nested inside Result / Option / containers / tuples
128        assert!(is_main_thread_bound_return(&ty(
129            "Result<BuiltDataFrame, String>"
130        )));
131        assert!(is_main_thread_bound_return(&ty(
132            "Result<DataFrameShape, std::string::String>"
133        )));
134        assert!(is_main_thread_bound_return(&ty("Option<BuiltDataFrame>")));
135        assert!(is_main_thread_bound_return(&ty("Vec<BuiltDataFrame>")));
136        assert!(is_main_thread_bound_return(&ty("(i32, BuiltDataFrame)")));
137        // Send-safe returns stay worker-eligible
138        assert!(!is_main_thread_bound_return(&ty("i32")));
139        assert!(!is_main_thread_bound_return(&ty(
140            "Result<Vec<f64>, String>"
141        )));
142        assert!(!is_main_thread_bound_return(&ty("ExternalPtr<MyType>")));
143        assert!(!is_main_thread_bound_return(&ty("DataFrame")));
144    }
145}
146
147/// Container family for a `several_ok` parameter, returned by
148/// [`classify_several_ok_container`].
149#[derive(Debug, Clone)]
150pub(crate) enum SeveralOkContainer {
151    /// `Vec<T>`
152    Vec,
153    /// `Box<[T]>`
154    BoxedSlice,
155    /// `[T; N]` — the `usize` is the fixed array length N
156    Array(usize),
157    /// `&[T]` or `&mut [T]` — allocate `Vec<T>` then borrow
158    BorrowedSlice,
159}
160
161/// Classify a `several_ok` parameter type into one of the four container
162/// families and extract its inner element type `T`.
163///
164/// Returns `Some((container, inner_ty))` or `None` if the type is not one of
165/// the four accepted container shapes.
166pub(crate) fn classify_several_ok_container(
167    ty: &syn::Type,
168) -> Option<(SeveralOkContainer, &syn::Type)> {
169    match ty {
170        // Vec<T>
171        syn::Type::Path(tp) => {
172            let seg = tp.path.segments.last()?;
173            if seg.ident == "Vec" {
174                let inner = first_type_argument(seg)?;
175                return Some((SeveralOkContainer::Vec, inner));
176            }
177            // Box<[T]>
178            if seg.ident == "Box"
179                && let syn::PathArguments::AngleBracketed(ab) = &seg.arguments
180            {
181                for arg in &ab.args {
182                    if let syn::GenericArgument::Type(syn::Type::Slice(s)) = arg {
183                        return Some((SeveralOkContainer::BoxedSlice, s.elem.as_ref()));
184                    }
185                }
186            }
187            None
188        }
189        // [T; N]
190        syn::Type::Array(arr) => {
191            if let syn::Expr::Lit(syn::ExprLit {
192                lit: syn::Lit::Int(n),
193                ..
194            }) = &arr.len
195            {
196                let n = n.base10_parse::<usize>().ok()?;
197                return Some((SeveralOkContainer::Array(n), arr.elem.as_ref()));
198            }
199            None
200        }
201        // &[T] or &mut [T]
202        syn::Type::Reference(r) => {
203            if let syn::Type::Slice(s) = r.elem.as_ref() {
204                return Some((SeveralOkContainer::BorrowedSlice, s.elem.as_ref()));
205            }
206            None
207        }
208        _ => None,
209    }
210}