miniextendr_macros/dataframe_derive.rs
1//! Derive macros for bidirectional row ↔ dataframe conversions.
2//!
3//! Supports both structs (direct field mapping) and enums (field-name union
4//! across variants with `Option<T>` fill for missing fields).
5
6use proc_macro2::{Span, TokenStream};
7use quote::{format_ident, quote};
8use syn::{Data, DeriveInput, Fields};
9
10// region: Attribute parsing
11
12/// Parsed container-level `#[dataframe(...)]` attributes.
13pub(super) struct DataFrameAttrs {
14 /// Custom companion type name (default: `{TypeName}DataFrame`).
15 pub(super) name: Option<syn::Ident>,
16 /// Enum alignment mode — implicit for enums, accepted but not required.
17 pub(super) align: bool,
18 /// Tag column name for variant discriminator (also supported on structs).
19 pub(super) tag: Option<String>,
20 /// Conflict resolution mode for type collisions across enum variants.
21 /// Currently only "string" is supported: convert conflicting fields via `ToString`.
22 pub(super) conflicts: Option<String>,
23}
24
25/// Parse container-level `#[dataframe(...)]` attributes from the derive input.
26///
27/// Supported keys:
28/// - `name = "CustomName"` -- custom companion type name (default: `{TypeName}DataFrame`)
29/// - `align` -- enum alignment mode (field-name union across variants)
30/// - `tag = "col_name"` -- add a variant discriminator column (works on both structs and enums)
31/// - `conflicts = "string"` -- coerce type-conflicting columns to `String` via `ToString`
32///
33/// Returns `Err` for unknown keys or non-string-literal values.
34fn parse_dataframe_attrs(input: &DeriveInput) -> syn::Result<DataFrameAttrs> {
35 let mut attrs = DataFrameAttrs {
36 name: None,
37 align: false,
38 tag: None,
39 conflicts: None,
40 };
41
42 for attr in &input.attrs {
43 if !attr.path().is_ident("dataframe") {
44 continue;
45 }
46
47 let nested = attr.parse_args_with(
48 syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
49 )?;
50
51 for meta in &nested {
52 match meta {
53 syn::Meta::NameValue(nv) if nv.path.is_ident("name") => {
54 if let syn::Expr::Lit(syn::ExprLit {
55 lit: syn::Lit::Str(lit_str),
56 ..
57 }) = &nv.value
58 {
59 attrs.name =
60 Some(format_ident!("{}", lit_str.value(), span = lit_str.span()));
61 } else {
62 return Err(syn::Error::new_spanned(
63 &nv.value,
64 "expected string literal for `name`",
65 ));
66 }
67 }
68 syn::Meta::NameValue(nv) if nv.path.is_ident("tag") => {
69 if let syn::Expr::Lit(syn::ExprLit {
70 lit: syn::Lit::Str(lit_str),
71 ..
72 }) = &nv.value
73 {
74 attrs.tag = Some(lit_str.value());
75 } else {
76 return Err(syn::Error::new_spanned(
77 &nv.value,
78 "expected string literal for `tag`",
79 ));
80 }
81 }
82 syn::Meta::NameValue(nv) if nv.path.is_ident("conflicts") => {
83 if let syn::Expr::Lit(syn::ExprLit {
84 lit: syn::Lit::Str(lit_str),
85 ..
86 }) = &nv.value
87 {
88 let value = lit_str.value();
89 if value != "string" {
90 return Err(syn::Error::new_spanned(
91 lit_str,
92 "unknown conflict resolution mode; only `\"string\"` is supported",
93 ));
94 }
95 attrs.conflicts = Some(value);
96 } else {
97 return Err(syn::Error::new_spanned(
98 &nv.value,
99 "expected string literal for `conflicts`",
100 ));
101 }
102 }
103 syn::Meta::Path(path) if path.is_ident("align") => {
104 attrs.align = true;
105 }
106 other => {
107 return Err(syn::Error::new_spanned(
108 other,
109 "unknown dataframe attribute; expected `name`, `align`, `tag`, or `conflicts`",
110 ));
111 }
112 }
113 }
114 }
115
116 Ok(attrs)
117}
118// endregion
119
120// region: Field-level attribute parsing
121
122/// Parsed field-level `#[dataframe(...)]` attributes.
123///
124/// These attributes control how individual struct/enum fields map to DataFrame columns.
125/// Mutually exclusive combinations (`as_list` + `expand`, `as_list` + `width`,
126/// `as_factor` + `as_list`, `as_factor` + `expand`, `as_factor` + `width`) are
127/// rejected during parsing.
128#[derive(Default)]
129pub(super) struct FieldAttrs {
130 /// `#[dataframe(skip)]` -- omit this field from the DataFrame entirely.
131 pub(super) skip: bool,
132 /// `#[dataframe(rename = "col")]` -- use a custom column name instead of the field name.
133 pub(super) rename: Option<String>,
134 /// `#[dataframe(as_list)]` -- keep a collection field as a single R list column
135 /// (suppresses automatic expansion into suffixed columns).
136 pub(super) as_list: bool,
137 /// `#[dataframe(as_factor)]` -- treat a unit-only inner enum field as an R factor column.
138 /// Only valid on bare-ident enum types (no generic parameters). The inner enum must be
139 /// unit-only (`#[derive(DataFrameRow)]` emits `IntoR` and `IntoR for Vec<Option<Self>>`).
140 pub(super) as_factor: bool,
141 /// `#[dataframe(expand)]` or `#[dataframe(unnest)]` -- explicitly expand a
142 /// collection field into multiple suffixed columns.
143 expand: bool,
144 /// `#[dataframe(width = N)]` -- pin the expansion width for `Vec<T>`, `Box<[T]>`,
145 /// or `&[T]` fields. Rows shorter than `N` get `None` for missing positions.
146 pub(super) width: Option<usize>,
147}
148
149/// Parse field-level `#[dataframe(...)]` attributes from a `syn::Field`.
150///
151/// Recognizes: `skip`, `rename`, `as_list`, `as_factor`, `expand` (alias `unnest`), and `width`.
152/// Validates mutual exclusivity of conflicting options (`as_list` vs `expand`/`width`,
153/// `as_factor` vs `as_list`/`expand`/`width`).
154/// Returns `Err` for unknown keys, invalid width values, or conflicting options.
155pub(super) fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
156 let mut attrs = FieldAttrs::default();
157
158 for attr in &field.attrs {
159 if !attr.path().is_ident("dataframe") {
160 continue;
161 }
162
163 attr.parse_nested_meta(|meta| {
164 if meta.path.is_ident("skip") {
165 attrs.skip = true;
166 Ok(())
167 } else if meta.path.is_ident("rename") {
168 let value = meta.value()?;
169 let lit: syn::LitStr = value.parse()?;
170 attrs.rename = Some(lit.value());
171 Ok(())
172 } else if meta.path.is_ident("as_list") {
173 attrs.as_list = true;
174 Ok(())
175 } else if meta.path.is_ident("as_factor") {
176 attrs.as_factor = true;
177 Ok(())
178 } else if meta.path.is_ident("expand") || meta.path.is_ident("unnest") {
179 attrs.expand = true;
180 Ok(())
181 } else if meta.path.is_ident("width") {
182 let value = meta.value()?;
183 let lit: syn::LitInt = value.parse()?;
184 let n: usize = lit.base10_parse()?;
185 if n == 0 {
186 return Err(syn::Error::new(lit.span(), "`width` must be >= 1"));
187 }
188 attrs.width = Some(n);
189 Ok(())
190 } else {
191 Err(meta.error(
192 "unknown field attribute; expected `skip`, `rename`, `as_list`, `as_factor`, `expand`, `unnest`, or `width`",
193 ))
194 }
195 })?;
196 }
197
198 let span = field.ident.as_ref().map_or(Span::call_site(), |i| i.span());
199
200 // Validation: conflicting options
201 if attrs.as_list && attrs.expand {
202 return Err(syn::Error::new(
203 span,
204 "`as_list` and `expand`/`unnest` are mutually exclusive",
205 ));
206 }
207 if attrs.as_list && attrs.width.is_some() {
208 return Err(syn::Error::new(
209 span,
210 "`as_list` and `width` are mutually exclusive",
211 ));
212 }
213 if attrs.as_factor && attrs.as_list {
214 return Err(syn::Error::new(
215 span,
216 "`as_factor` and `as_list` are mutually exclusive",
217 ));
218 }
219 if attrs.as_factor && attrs.expand {
220 return Err(syn::Error::new(
221 span,
222 "`as_factor` and `expand`/`unnest` are mutually exclusive",
223 ));
224 }
225 if attrs.as_factor && attrs.width.is_some() {
226 return Err(syn::Error::new(
227 span,
228 "`as_factor` and `width` are mutually exclusive",
229 ));
230 }
231
232 Ok(attrs)
233}
234// endregion
235
236// region: Type classification
237
238/// Classification of a field type for DataFrame column expansion.
239///
240/// Used to decide whether a field maps to a single column or should be
241/// expanded into multiple suffixed columns (e.g., `coords_1`, `coords_2`).
242pub(super) enum FieldTypeKind<'a> {
243 /// Single column (most types). No expansion.
244 Scalar,
245 /// `[T; N]` -- fixed-size array, expands to `N` columns at compile time.
246 /// Contains the element type and array length.
247 FixedArray(&'a syn::Type, usize),
248 /// `Vec<T>` -- variable length, needs `width` attribute or `expand` for expansion.
249 /// Contains the element type.
250 VariableVec(&'a syn::Type),
251 /// `Box<[T]>` -- owned slice, treated like `Vec<T>` for expansion purposes.
252 /// Contains the element type.
253 BoxedSlice(&'a syn::Type),
254 /// `&[T]` -- borrowed slice, treated like `Vec<T>` for expansion purposes.
255 /// Contains the element type.
256 BorrowedSlice(&'a syn::Type),
257 /// `HashMap<K, V>` or `BTreeMap<K, V>`. The two derive paths treat maps
258 /// differently:
259 /// - *enum path*: expands to two parallel list-columns `<field>_keys` /
260 /// `<field>_values` (see `enum_expansion.rs`).
261 /// - *struct path*: resolves to a `Single` opaque list-of-named-lists
262 /// column (`Vec<map>: IntoR`); reader-capable for `String` keys +
263 /// reader-scalar values (#764, see `SingleFieldData::map_reader`).
264 ///
265 /// Key order follows the map's own iteration order: `BTreeMap` yields
266 /// sorted keys, `HashMap` yields non-deterministic order.
267 Map {
268 key_ty: &'a syn::Type,
269 val_ty: &'a syn::Type,
270 },
271 /// A struct-typed field whose inner type implements `DataFrameRow`.
272 ///
273 /// Flattened into `<field>_<inner_col>` prefixed columns by default.
274 /// A compile-time assertion against `::miniextendr_api::markers::DataFrameRow`
275 /// is emitted so rustc gives a clear error when the inner type is missing the
276 /// derive.
277 ///
278 /// Suppressed by `#[dataframe(as_list)]` — with as_list the field becomes
279 /// a `Scalar` and uses the ordinary single-column codegen path.
280 Struct {
281 /// The full field type (used for the compile-time DataFrameRow assertion).
282 inner_ty: &'a syn::Type,
283 },
284}
285
286/// Classify a field type for DataFrame column expansion.
287///
288/// Inspects the type AST to detect:
289/// - `[T; N]` or `&[T; N]` -> `FixedArray`
290/// - `&[T]` -> `BorrowedSlice`
291/// - `Vec<T>` -> `VariableVec`
292/// - `Box<[T]>` -> `BoxedSlice`
293/// - `HashMap<K, V>` / `BTreeMap<K, V>` -> `Map`
294/// - Any non-scalar bare path type (single- or multi-segment, e.g. `Point` or
295/// `crate::geom::Point`) -> `Struct`
296/// - Everything else (known scalars, generic types with args, `::abs::Paths`) -> `Scalar`
297///
298/// Returns `Err` for shapes the macro cannot classify and that would silently
299/// become opaque list-columns: `Option<T>`, `Cow<T>`, `Rc<T>`, `Arc<T>`,
300/// `RefCell<T>`, `Cell<T>`, `Mutex<T>`, `RwLock<T>`. Use
301/// `#[dataframe(as_list)]` to opt into list-column treatment explicitly.
302pub(super) fn classify_field_type(ty: &syn::Type) -> syn::Result<FieldTypeKind<'_>> {
303 // Check for [T; N]
304 if let syn::Type::Array(arr) = ty
305 && let syn::Expr::Lit(syn::ExprLit {
306 lit: syn::Lit::Int(lit_int),
307 ..
308 }) = &arr.len
309 && let Ok(n) = lit_int.base10_parse::<usize>()
310 {
311 return Ok(FieldTypeKind::FixedArray(&arr.elem, n));
312 }
313
314 // Check for &[T] and &[T; N]
315 if let syn::Type::Reference(ref_ty) = ty {
316 // &[T] → BorrowedSlice
317 if let syn::Type::Slice(slice) = &*ref_ty.elem {
318 return Ok(FieldTypeKind::BorrowedSlice(&slice.elem));
319 }
320 // &[T; N] → FixedArray (same as owned)
321 if let syn::Type::Array(arr) = &*ref_ty.elem
322 && let syn::Expr::Lit(syn::ExprLit {
323 lit: syn::Lit::Int(lit_int),
324 ..
325 }) = &arr.len
326 && let Ok(n) = lit_int.base10_parse::<usize>()
327 {
328 return Ok(FieldTypeKind::FixedArray(&arr.elem, n));
329 }
330 }
331
332 if let syn::Type::Path(type_path) = ty
333 && let Some(seg) = type_path.path.segments.last()
334 && let syn::PathArguments::AngleBracketed(args) = &seg.arguments
335 {
336 // Reject wrapper types that would silently fall through to Scalar /
337 // Struct and produce a confusing opaque list-column or a downstream
338 // DataFrameRow assertion error. These are the common smart-pointer
339 // and interior-mutability types that wrap a meaningful inner type but
340 // that DataFrameRow does not know how to expand.
341 //
342 // The macro has no way to resolve through the wrapper without type-
343 // checking (which is unavailable in proc macros). The user must either
344 // unwrap to the inner type, or annotate with `#[dataframe(as_list)]`
345 // to opt into an explicit opaque list-column.
346 //
347 // IMPORTANT: The rejection fires on *path identity alone*, before we
348 // inspect generic args. `Cow<'a, T>` has a lifetime as its first
349 // generic argument, not a type; inspecting `args.args.first()` as a
350 // `GenericArgument::Type` would silently skip `Cow`. Checking ident
351 // before args makes the rejection robust to any generic shape.
352 const REJECTED_WRAPPERS: &[&str] = &[
353 "Option", "Cow", "Rc", "Arc", "RefCell", "Cell", "Mutex", "RwLock",
354 ];
355 let name = seg.ident.to_string();
356 if REJECTED_WRAPPERS.contains(&name.as_str()) {
357 return Err(syn::Error::new_spanned(
358 ty,
359 format!(
360 "DataFrameRow does not support `{name}<…>` directly as a field type. \
361 Use `#[dataframe(as_list)]` to opt into an explicit opaque list-column, \
362 or unwrap to the inner type (e.g. store the inner value directly, using \
363 a sentinel / empty collection for the absent case)."
364 ),
365 ));
366 }
367
368 // For the collection types below we need the first *type* argument.
369 // Skip any leading lifetime or const arguments (e.g. `Cow<'a, B>`
370 // has a lifetime first, but `Cow` is already rejected above so we
371 // only reach here for other angle-bracketed types).
372 let first_type_arg = args.args.iter().find_map(|arg| {
373 if let syn::GenericArgument::Type(t) = arg {
374 Some(t)
375 } else {
376 None
377 }
378 });
379
380 if let Some(inner) = first_type_arg {
381 // Check for Vec<T>
382 if seg.ident == "Vec" {
383 return Ok(FieldTypeKind::VariableVec(inner));
384 }
385
386 // Check for Box<[T]>
387 if seg.ident == "Box"
388 && let syn::Type::Slice(slice) = inner
389 {
390 return Ok(FieldTypeKind::BoxedSlice(&slice.elem));
391 }
392
393 // Check for HashMap<K, V> and BTreeMap<K, V>
394 if (seg.ident == "HashMap" || seg.ident == "BTreeMap")
395 && let Some(syn::GenericArgument::Type(val_ty)) = args.args.iter().nth(1)
396 {
397 return Ok(FieldTypeKind::Map {
398 key_ty: inner,
399 val_ty,
400 });
401 }
402 }
403 }
404
405 // Any remaining path type whose LAST segment is a bare ident (no generic args)
406 // that is NOT a known scalar is treated as a user-defined struct whose
407 // `DataFrameRow` derive should be called. The compile-time assertion
408 // `_assert_inner_is_dataframe_row::<Inner>()` in the generated code surfaces a
409 // clear error if the inner type doesn't have the derive.
410 //
411 // Known scalars (i32, f64, String, bool, …) are kept as `Scalar` so that existing
412 // enum variants with primitive fields (e.g. `Click { id: i64, x: f64 }`) are not
413 // misclassified as struct fields.
414 //
415 // Multi-segment paths (e.g. `crate::geom::Point`, `geom::Point`) are now correctly
416 // classified here — the previous `segs.len() == 1` guard was overly restrictive.
417 // Paths with a leading `::` (absolute paths like `::std::ffi::CString`) still fall
418 // through to `Scalar`; use `#[dataframe(as_list)]` or an unqualified import if
419 // you need a custom treatment.
420 //
421 // RISK: a user type whose last path segment is named after a known-scalar
422 // (e.g. `mymod::String`) still correctly falls through to `Scalar` because of the
423 // KNOWN_SCALARS check. A type named `mymod::Option` / `mymod::Vec` would shadow
424 // the detection above — accepted per Rust naming convention (canonical names are
425 // rarely shadowed). `#[dataframe(as_list)]` is the documented escape hatch.
426 if let syn::Type::Path(type_path) = ty {
427 let segs = &type_path.path.segments;
428 // No leading colon (rules out `::std::…` absolute paths) and no self-type.
429 if type_path.qself.is_none() && type_path.path.leading_colon.is_none() {
430 let seg = segs.last().unwrap();
431 if matches!(seg.arguments, syn::PathArguments::None) {
432 let name = seg.ident.to_string();
433 // Known scalar type names — keep as Scalar so they do not trigger the
434 // struct-flatten path and the DataFrameRow compile-time assertion.
435 const KNOWN_SCALARS: &[&str] = &[
436 "bool", "char", "str", "f32", "f64", "i8", "i16", "i32", "i64", "i128",
437 "isize", "u8", "u16", "u32", "u64", "u128", "usize", "String",
438 ];
439 if !KNOWN_SCALARS.contains(&name.as_str()) {
440 return Ok(FieldTypeKind::Struct { inner_ty: ty });
441 }
442 }
443 }
444 }
445
446 Ok(FieldTypeKind::Scalar)
447}
448
449/// Scalar element types whose `Vec<T>` (and `Vec<Option<T>>`) round-trips through
450/// `TryFromSexp` — the supported field types for the parallel from-R reader
451/// (`try_from_dataframe_par`). This is intentionally narrower than
452/// `FieldTypeKind::Scalar`: set/opaque collection types (`HashSet<…>`,
453/// `BTreeSet<…>`) also classify as `Scalar` but do NOT implement `Vec<_>:
454/// TryFromSexp`, so they must be excluded from the reader path.
455///
456/// `pub(super)` so `enum_expansion.rs` (in the `dataframe_derive` module dir)
457/// can reuse the same allow-list without duplication.
458pub(super) const READER_SCALAR_NAMES: &[&str] = &[
459 "bool", "f32", "f64", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "String",
460];
461
462/// True if `ty` is a bare known-scalar ident (no `Option`, no generic args).
463///
464/// These are the element types whose `Vec<Option<ty>>` round-trips through
465/// `TryFromSexp` — required for the *column-expansion* readers, which read each
466/// expanded slot as `Vec<Option<elem>>` (the write side wraps every slot in
467/// `Option`). Allowing `Option<scalar>` here would ask for `Vec<Option<Option<…>>>`,
468/// which has no `TryFromSexp` impl.
469///
470/// `pub(super)` so `enum_expansion.rs` can reuse it.
471pub(super) fn is_bare_reader_scalar_ty(ty: &syn::Type) -> bool {
472 if let syn::Type::Path(tp) = ty
473 && tp.qself.is_none()
474 && tp.path.leading_colon.is_none()
475 && let Some(seg) = tp.path.segments.last()
476 && matches!(seg.arguments, syn::PathArguments::None)
477 {
478 return READER_SCALAR_NAMES.contains(&seg.ident.to_string().as_str());
479 }
480 false
481}
482
483/// True if `ty` is a bare known-scalar ident, or `Option<bare-known-scalar>`.
484///
485/// These are exactly the field types for which the from-R reader can pull a
486/// column out as `Vec<ty>` via `TryFromSexp` (scalar `Single` fields and
487/// `[T; N]` fixed-array elements, neither of which adds an `Option` wrapper).
488///
489/// `pub(super)` so `enum_expansion.rs` can reuse it.
490pub(super) fn is_reader_scalar_ty(ty: &syn::Type) -> bool {
491 if is_bare_reader_scalar_ty(ty) {
492 return true;
493 }
494 // Option<scalar>
495 if let syn::Type::Path(tp) = ty
496 && let Some(seg) = tp.path.segments.last()
497 && seg.ident == "Option"
498 && let syn::PathArguments::AngleBracketed(args) = &seg.arguments
499 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
500 {
501 return is_bare_reader_scalar_ty(inner);
502 }
503 false
504}
505
506/// True if `ty`'s last path segment is the bare ident `String` (path-identity
507/// check, same convention as `classify_field_type`).
508fn is_string_ty(ty: &syn::Type) -> bool {
509 if let syn::Type::Path(tp) = ty
510 && let Some(seg) = tp.path.segments.last()
511 && matches!(seg.arguments, syn::PathArguments::None)
512 {
513 return seg.ident == "String";
514 }
515 false
516}
517
518/// Number of *type* arguments on `ty`'s last path segment (0 when not
519/// angle-bracketed). Rejects `HashMap<K, V, S>` custom-hasher maps from the
520/// reader path — the `Vec<HashMap<String, V>>: TryFromSexp` impl only covers
521/// the default hasher.
522fn generic_type_arg_count(ty: &syn::Type) -> usize {
523 if let syn::Type::Path(tp) = ty
524 && let Some(seg) = tp.path.segments.last()
525 && let syn::PathArguments::AngleBracketed(args) = &seg.arguments
526 {
527 return args
528 .args
529 .iter()
530 .filter(|a| matches!(a, syn::GenericArgument::Type(_)))
531 .count();
532 }
533 0
534}
535
536/// True if a resolved struct field can be read back out of an R `data.frame`.
537///
538/// Determines whether the struct gets a generated `try_from_dataframe` reader.
539/// Each shape's reader is the inverse of its column-expansion write rule:
540/// - `Single` scalar: one column read as `Vec<ty>` (excludes `as_list` and
541/// opaque set/collection columns — those classify as `Single` but lack
542/// `Vec<_>: TryFromSexp`).
543/// - `Single` map column (`HashMap<String, V>` / `BTreeMap<String, V>`): one
544/// VECSXP list-of-named-lists column read whole via `Vec<map>: TryFromSexp`
545/// — the exact inverse of the `Vec<map>: IntoR` write shape (#764). Gated
546/// to `String` keys + reader-scalar values at resolve time (`map_reader`).
547/// - `Single` owned list-column: `Vec<scalar>` / `Box<[scalar]>` stored as a
548/// VECSXP list-column; the reader deserialises each row's element via
549/// `Vec<elem>: TryFromSexp` and `.into()`-converts to the field type (#809).
550/// - `ExpandedFixed` (`[T; N]`): `N` columns regrouped into the array.
551/// - `ExpandedVec` / `AutoExpandVec` (`Vec<T>`): suffixed `Option` columns
552/// flattened back per row (bare-scalar elements only).
553/// - `Struct` (nested `DataFrameRow`): always eligible — the reader routes the
554/// un-prefixed sub-frame through the inner type's `DataFrameRowConvert`, which
555/// degrades to a clear runtime error if the inner shape itself has no reader.
556///
557/// Borrowed expansion origins (`&[T]` / `&[T; N]`) are not readable (owned R data
558/// can't produce a borrow) — flagged via `readable` at resolve time.
559fn field_reader_capable(rf: &ResolvedField) -> bool {
560 match rf {
561 ResolvedField::Single(d) => {
562 !d.needs_into_list
563 && (is_reader_scalar_ty(&d.ty)
564 || d.map_reader
565 || d.list_elem_ty
566 .as_ref()
567 .is_some_and(is_bare_reader_scalar_ty))
568 }
569 ResolvedField::ExpandedFixed(d) => d.readable && is_reader_scalar_ty(&d.elem_ty),
570 ResolvedField::ExpandedVec(d) => d.readable && is_bare_reader_scalar_ty(&d.elem_ty),
571 ResolvedField::AutoExpandVec(d) => d.readable && is_bare_reader_scalar_ty(&d.elem_ty),
572 ResolvedField::Struct(_) => true,
573 ResolvedField::Map(d) => {
574 is_bare_reader_scalar_ty(&d.key_ty) && is_bare_reader_scalar_ty(&d.val_ty)
575 }
576 }
577}
578
579/// True if `ty` is a borrowed reference (`&[T]`, `&[T; N]`, `&str`, …). Such
580/// expansion fields can't be reconstructed by value in the R→Rust reader.
581fn field_is_borrowed_ref(ty: &syn::Type) -> bool {
582 matches!(ty, syn::Type::Reference(_))
583}
584// endregion
585
586// region: Resolved field model (struct path)
587
588/// A resolved struct field ready for codegen -- determines how this field maps
589/// to DataFrame companion struct columns.
590///
591/// Each variant represents a different expansion strategy:
592/// - `Single`: one field -> one `Vec<T>` column
593/// - `ExpandedFixed`: `[T; N]` -> N columns (`name_1..name_N`) at compile time
594/// - `ExpandedVec`: `Vec<T>` + `width = N` -> N `Vec<Option<T>>` columns
595/// - `AutoExpandVec`: `Vec<T>` + `expand` -> dynamic column count at runtime
596enum ResolvedField {
597 /// Single column: `name → Vec<ty>`.
598 Single(Box<SingleFieldData>),
599 /// Expanded fixed array: `name: [T; N]` → `name_1..name_N`.
600 ExpandedFixed(Box<ExpandedFixedData>),
601 /// Expanded variable vec with pinned width: `name: Vec<T>` + `width = N`.
602 ExpandedVec(Box<ExpandedVecData>),
603 /// Auto-expanded `Vec<T>`/`Box<[T]>`: column count determined at runtime from max row length.
604 AutoExpandVec(Box<AutoExpandVecData>),
605 /// Struct field whose inner type implements `DataFrameRow` (issue #485).
606 /// Companion holds `Vec<Inner>`; `into_data_frame` calls `Inner::to_dataframe`
607 /// and flattens columns under the `<base>_` prefix.
608 Struct(Box<StructFieldData>),
609 /// Non-String-keyed `HashMap<K,V>` / `BTreeMap<K,V>` field (#919).
610 /// Expands to two parallel list-columns `<base>_keys` / `<base>_values`,
611 /// each a `Vec<Vec<K>>` / `Vec<Vec<V>>` (VECSXP of typed vectors). The
612 /// reader zips `keys[i]` with `values[i]` back into the map type.
613 Map(Box<MapFieldData>),
614}
615
616/// Data for [`ResolvedField::Map`] (struct path, non-String-keyed maps, #919).
617struct MapFieldData {
618 /// Rust field name (for access on the row type).
619 rust_name: syn::Ident,
620 /// Column name base — keys col = `<base>_keys`, values col = `<base>_values`.
621 base_name: String,
622 /// Key type `K`.
623 key_ty: syn::Type,
624 /// Value type `V`.
625 val_ty: syn::Type,
626 /// Full map type `HashMap<K,V>` / `BTreeMap<K,V>` (used in reader zip).
627 map_ty: syn::Type,
628 /// Index in tuple struct (None for named).
629 tuple_index: Option<syn::Index>,
630}
631
632/// Data for [`ResolvedField::Single`].
633struct SingleFieldData {
634 /// Rust field name (for access).
635 rust_name: syn::Ident,
636 /// Column name in the DataFrame.
637 col_name: syn::Ident,
638 /// Column name string.
639 col_name_str: String,
640 /// Field type stored in the companion `Vec<#ty>`. For `#[dataframe(as_list)]`
641 /// on a struct-typed field this is overridden to `::miniextendr_api::list::List`
642 /// — see `needs_into_list`.
643 ty: syn::Type,
644 /// Index in tuple struct (None for named).
645 tuple_index: Option<syn::Index>,
646 /// `#[dataframe(as_list)]` on a struct-typed field (#485 workaround).
647 /// When `true`, the companion field type is overridden to `List` and
648 /// `From<Vec<Row>>` calls `IntoList::into_list()` on each row value.
649 needs_into_list: bool,
650 /// `Some(elem)` when this Single field is an un-annotated *owned* collection
651 /// (`Vec<scalar>` / `Box<[scalar]>`) stored as an opaque list-column (#809).
652 /// The reader deserialises the list-column back into the owned collection per
653 /// row via `Vec<elem>: TryFromSexp` then `.into()` to the field container type.
654 /// `None` for scalar Single, `as_list`, opaque `Map`/set columns, and borrowed
655 /// `&[T]` (not readable).
656 list_elem_ty: Option<syn::Type>,
657 /// `true` when this Single field is a reader-capable map column (#764):
658 /// `HashMap<String, V>` / `BTreeMap<String, V>` with a reader-scalar `V`
659 /// and no custom hasher. The column is read whole as `Vec<#ty>` via the
660 /// `Vec<map>: TryFromSexp` list-of-named-lists impl — the same `pull_col`
661 /// path scalar Singles use, so it only widens the capability gate.
662 map_reader: bool,
663}
664
665/// Data for [`ResolvedField::ExpandedFixed`].
666struct ExpandedFixedData {
667 /// Rust field name.
668 rust_name: syn::Ident,
669 /// Base column name (before suffix).
670 base_name: String,
671 /// Element type T.
672 elem_ty: syn::Type,
673 /// Array length N.
674 len: usize,
675 /// Index in tuple struct.
676 tuple_index: Option<syn::Index>,
677 /// Whether the field can be reconstructed by value in the R→Rust reader.
678 /// `false` for a borrowed origin (`&[T; N]`) — owned R data can't produce a
679 /// borrow, so the struct gets no reader (see [`field_reader_capable`]).
680 readable: bool,
681}
682
683/// Data for [`ResolvedField::ExpandedVec`].
684struct ExpandedVecData {
685 /// Rust field name.
686 rust_name: syn::Ident,
687 /// Base column name.
688 base_name: String,
689 /// Element type T.
690 elem_ty: syn::Type,
691 /// Pinned width.
692 width: usize,
693 /// Index in tuple struct.
694 tuple_index: Option<syn::Index>,
695 /// Whether the field can be reconstructed by value in the R→Rust reader.
696 /// `false` for a borrowed origin (`&[T]`); `Vec<T>` / `Box<[T]>` are readable
697 /// (the reader collects a `Vec<T>` and `.into()`-converts to the field type).
698 readable: bool,
699}
700
701/// Data for [`ResolvedField::Struct`].
702///
703/// A struct field whose inner type implements `DataFrameRow`. The companion
704/// struct holds `Vec<Inner>` (the same type users already pass into
705/// `to_dataframe(vec![...])`). At `into_data_frame()` time the inner rows are
706/// converted via `Inner::to_dataframe` → `into_named_columns()`, prefixed with
707/// `<base_name>_`, and pushed into the parent data.frame.
708struct StructFieldData {
709 /// Rust field name (for access on the row type).
710 rust_name: syn::Ident,
711 /// Companion struct field name (ident).
712 col_name: syn::Ident,
713 /// Column name base used as the R-side prefix (`<base>_<inner_col>`).
714 col_name_str: String,
715 /// Inner struct type (used for `to_dataframe` dispatch + DataFrameRow assertion).
716 inner_ty: syn::Type,
717 /// Index in tuple struct (None for named).
718 tuple_index: Option<syn::Index>,
719}
720
721/// Data for [`ResolvedField::AutoExpandVec`].
722struct AutoExpandVecData {
723 /// Rust field name (for row access).
724 rust_name: syn::Ident,
725 /// Companion struct field name (ident).
726 col_name: syn::Ident,
727 /// Column name base string (for suffixed column names).
728 col_name_str: String,
729 /// Element type T.
730 elem_ty: syn::Type,
731 /// Container type for companion struct (`Vec<T>` or `Box<[T]>`).
732 container_ty: syn::Type,
733 /// Index in tuple struct.
734 tuple_index: Option<syn::Index>,
735 /// Whether the field can be reconstructed by value in the R→Rust reader.
736 /// `false` for a borrowed origin (`&[T]`); `Vec<T>` / `Box<[T]>` are readable.
737 readable: bool,
738}
739
740/// Resolve a struct field into a [`ResolvedField`], applying field attributes.
741///
742/// Combines the field's `#[dataframe(...)]` attributes with its type classification
743/// to determine the codegen strategy:
744/// - `skip` -> returns `None`
745/// - `as_list` -> `Single` (suppresses expansion)
746/// - `FixedArray` -> `ExpandedFixed` (compile-time expansion to N columns)
747/// - `VariableVec`/`BoxedSlice`/`BorrowedSlice` + `width` -> `ExpandedVec`
748/// - `VariableVec`/`BoxedSlice`/`BorrowedSlice` + `expand` -> `AutoExpandVec`
749/// - Everything else -> `Single`
750///
751/// Returns `Err` if `width` or `expand` is used on an incompatible type.
752fn resolve_struct_field(
753 field: &syn::Field,
754 index: usize,
755 is_tuple: bool,
756) -> syn::Result<Option<ResolvedField>> {
757 let field_attrs = parse_field_attrs(field)?;
758
759 if field_attrs.skip {
760 return Ok(None);
761 }
762
763 let rust_name = if is_tuple {
764 format_ident!("_{}", index)
765 } else {
766 field.ident.as_ref().unwrap().clone()
767 };
768
769 let col_name_str = field_attrs
770 .rename
771 .clone()
772 .unwrap_or_else(|| rust_name.to_string());
773 let col_name = format_ident!("{}", col_name_str);
774
775 let tuple_index = if is_tuple {
776 Some(syn::Index::from(index))
777 } else {
778 None
779 };
780
781 let ty = &field.ty;
782 // Propagate classification errors (e.g. Option<T>, Arc<T>) when as_list is
783 // not set. The as_list branch below uses `.ok()` to suppress errors.
784 let kind = classify_field_type(ty);
785
786 // as_list suppresses expansion. For struct-typed fields (#485 opt-out), the
787 // companion stores `Vec<List>` and From<Vec<Row>> converts each row value
788 // via `IntoList::into_list()`. For non-struct as_list fields, the existing
789 // behavior is preserved: companion stores `Vec<#ty>` and the field type is
790 // serialized natively (this requires `Vec<#ty>: IntoR`).
791 if field_attrs.as_list {
792 // Use `.ok()` here: `as_list` is an explicit opt-in, so wrapper types
793 // like `Option<T>` / `Arc<T>` are allowed — they become opaque list-
794 // columns. Any classification error is suppressed and treated as non-Struct.
795 let (final_ty, needs_into_list) = match classify_field_type(ty).ok() {
796 Some(FieldTypeKind::Struct { .. }) => {
797 (syn::parse_quote!(::miniextendr_api::list::List), true)
798 }
799 _ => (ty.clone(), false),
800 };
801 return Ok(Some(ResolvedField::Single(Box::new(SingleFieldData {
802 rust_name,
803 col_name,
804 col_name_str,
805 ty: final_ty,
806 tuple_index,
807 needs_into_list,
808 list_elem_ty: None,
809 map_reader: false,
810 }))));
811 }
812
813 match kind? {
814 FieldTypeKind::FixedArray(elem_ty, len) => Ok(Some(ResolvedField::ExpandedFixed(
815 Box::new(ExpandedFixedData {
816 rust_name,
817 base_name: col_name_str,
818 elem_ty: elem_ty.clone(),
819 len,
820 tuple_index,
821 readable: !field_is_borrowed_ref(ty),
822 }),
823 ))),
824 FieldTypeKind::VariableVec(elem_ty)
825 | FieldTypeKind::BoxedSlice(elem_ty)
826 | FieldTypeKind::BorrowedSlice(elem_ty) => {
827 if let Some(width) = field_attrs.width {
828 Ok(Some(ResolvedField::ExpandedVec(Box::new(
829 ExpandedVecData {
830 rust_name,
831 base_name: col_name_str,
832 elem_ty: elem_ty.clone(),
833 width,
834 tuple_index,
835 readable: !field_is_borrowed_ref(ty),
836 },
837 ))))
838 } else if field_attrs.expand {
839 Ok(Some(ResolvedField::AutoExpandVec(Box::new(
840 AutoExpandVecData {
841 rust_name,
842 col_name,
843 col_name_str,
844 elem_ty: elem_ty.clone(),
845 container_ty: ty.clone(),
846 tuple_index,
847 readable: !field_is_borrowed_ref(ty),
848 },
849 ))))
850 } else {
851 // No expansion — keep as opaque single column (list-column on R side).
852 // Readable owned collections (`Vec<scalar>` / `Box<[scalar]>`) record
853 // the element type for the list-column reader (#809). Borrowed `&[T]`
854 // is not readable (can't produce a borrow from owned R data).
855 Ok(Some(ResolvedField::Single(Box::new(SingleFieldData {
856 rust_name,
857 col_name,
858 col_name_str,
859 ty: ty.clone(),
860 tuple_index,
861 needs_into_list: false,
862 list_elem_ty: if field_is_borrowed_ref(ty) {
863 None
864 } else {
865 Some((*elem_ty).clone())
866 },
867 map_reader: false,
868 }))))
869 }
870 }
871 // Struct-in-struct flattening (issue #485): inner type must implement
872 // `DataFrameRow`. Flattening happens at `into_data_frame()` time; the
873 // companion stores `Vec<Inner>`. `as_list` opts out (handled above).
874 FieldTypeKind::Struct { inner_ty } => {
875 if field_attrs.width.is_some() {
876 return Err(syn::Error::new_spanned(
877 ty,
878 "`width` is only valid on `Vec<T>`, `Box<[T]>`, or `&[T]` fields",
879 ));
880 }
881 if field_attrs.expand {
882 return Err(syn::Error::new_spanned(
883 ty,
884 "`expand`/`unnest` is only valid on `[T; N]`, `Vec<T>`, `Box<[T]>`, or `&[T]` fields",
885 ));
886 }
887 Ok(Some(ResolvedField::Struct(Box::new(StructFieldData {
888 rust_name,
889 col_name,
890 col_name_str,
891 inner_ty: inner_ty.clone(),
892 tuple_index,
893 }))))
894 }
895 kind @ (FieldTypeKind::Scalar | FieldTypeKind::Map { .. }) => {
896 if field_attrs.width.is_some() {
897 return Err(syn::Error::new_spanned(
898 ty,
899 "`width` is only valid on `Vec<T>`, `Box<[T]>`, or `&[T]` fields",
900 ));
901 }
902 if field_attrs.expand {
903 return Err(syn::Error::new_spanned(
904 ty,
905 "`expand`/`unnest` is only valid on `[T; N]`, `Vec<T>`, `Box<[T]>`, or `&[T]` fields",
906 ));
907 }
908 // Struct-path map fields:
909 //
910 // - `String`-keyed + reader-scalar value + 2 type args (default hasher):
911 // write as one opaque list-of-named-lists column (`Vec<map>: IntoR`).
912 // Reader-capable via `Vec<map>: TryFromSexp` (#764). Falls through
913 // to `Single` with `map_reader: true` — unchanged.
914 //
915 // - Non-String bare-reader-scalar key + bare-reader-scalar value + 2 type args:
916 // expand to two parallel list-columns `<base>_keys` / `<base>_values` (#919).
917 // `Vec<Vec<K>>: IntoR` and `Vec<Vec<V>>: IntoR` work via the `T: RNativeType`
918 // blanket. Float keys (`f32`/`f64`) are also bare-reader-scalar but lack
919 // `Eq + Hash` / `Ord`, so reject them with a clear error.
920 //
921 // - Custom hasher (3+ type args): fall through to `Single` with no reader
922 // (keeps existing behaviour for `HashMap<K, V, S>`).
923 //
924 // - Non-scalar key or value: emit a clear error directing to `as_list`.
925 if let FieldTypeKind::Map { key_ty, val_ty } = kind {
926 let two_args = generic_type_arg_count(ty) == 2;
927 if is_string_ty(key_ty) && is_reader_scalar_ty(val_ty) && two_args {
928 // String-keyed — existing path (#764).
929 return Ok(Some(ResolvedField::Single(Box::new(SingleFieldData {
930 rust_name,
931 col_name,
932 col_name_str,
933 ty: ty.clone(),
934 tuple_index,
935 needs_into_list: false,
936 list_elem_ty: None,
937 map_reader: true,
938 }))));
939 }
940 // Float key check: f32/f64 classify as bare_reader_scalar but are
941 // neither Eq+Hash nor Ord, so they can't be a map key at all.
942 let is_float_ty = |t: &syn::Type| -> bool {
943 if let syn::Type::Path(tp) = t
944 && let Some(seg) = tp.path.segments.last()
945 && matches!(seg.arguments, syn::PathArguments::None)
946 {
947 let n = seg.ident.to_string();
948 return n == "f32" || n == "f64";
949 }
950 false
951 };
952 if is_float_ty(key_ty) {
953 return Err(syn::Error::new_spanned(
954 ty,
955 "HashMap/BTreeMap with float keys is not supported \
956 (f32/f64 are not Eq+Hash/Ord); use a newtype wrapper \
957 or `#[dataframe(as_list)]`",
958 ));
959 }
960 if two_args && is_bare_reader_scalar_ty(key_ty) && is_bare_reader_scalar_ty(val_ty)
961 {
962 // Non-String bare-scalar keyed — new parallel _keys/_values path (#919).
963 return Ok(Some(ResolvedField::Map(Box::new(MapFieldData {
964 rust_name,
965 base_name: col_name_str,
966 key_ty: key_ty.clone(),
967 val_ty: val_ty.clone(),
968 map_ty: ty.clone(),
969 tuple_index,
970 }))));
971 }
972 if two_args
973 && (!is_bare_reader_scalar_ty(key_ty) || !is_bare_reader_scalar_ty(val_ty))
974 {
975 // Non-scalar key or value (and not String-keyed) — opaque, no reader.
976 // Fall through to Single below.
977 }
978 // 3+ type args (custom hasher) or non-scalar — Single with no reader.
979 }
980 let map_reader = false;
981 Ok(Some(ResolvedField::Single(Box::new(SingleFieldData {
982 rust_name,
983 col_name,
984 col_name_str,
985 ty: ty.clone(),
986 tuple_index,
987 needs_into_list: false,
988 list_elem_ty: None,
989 map_reader,
990 }))))
991 }
992 }
993}
994// endregion
995
996// region: Top-level dispatch
997
998/// Derive `DataFrameRow`: generates a companion DataFrame type with collection fields.
999///
1000/// # Requirements
1001///
1002/// For structs: the type must implement `IntoList`.
1003/// For enums: all variants must have named fields.
1004///
1005/// # Generated Items
1006///
1007/// For a struct `Measurement { time: f64, value: f64 }`:
1008/// - Struct `MeasurementDataFrame { time: Vec<f64>, value: Vec<f64> }`
1009/// - `impl IntoDataFrame for MeasurementDataFrame`
1010/// - `impl From<Vec<Measurement>> for MeasurementDataFrame`
1011/// - `impl IntoIterator for MeasurementDataFrame`
1012/// - Associated methods on `Measurement`:
1013/// - `to_dataframe(Vec<Self>) -> MeasurementDataFrame`
1014/// - `from_dataframe(MeasurementDataFrame) -> Vec<Self>`
1015///
1016/// For an enum:
1017/// - Companion struct with `Vec<Option<T>>` columns (field-name union)
1018/// - Optional tag column for variant discrimination
1019/// - `impl From<Vec<Enum>> for EnumDataFrame`
1020/// - `impl IntoDataFrame for EnumDataFrame`
1021/// - Associated `to_dataframe` method
1022///
1023/// # Attributes
1024///
1025/// - `#[dataframe(name = "CustomName")]` — Custom companion type name
1026/// - `#[dataframe(align)]` — Enum alignment mode (accepted but implicit)
1027/// - `#[dataframe(tag = "col")]` — Add variant discriminator column
1028///
1029/// Both struct and enum companion types get `from_rows()` (sequential) and
1030/// `from_rows_par()` (parallel, `#[cfg(feature = "rayon")]`) methods automatically.
1031pub fn derive_dataframe_row(input: DeriveInput) -> syn::Result<TokenStream> {
1032 let row_name = &input.ident;
1033
1034 // Allow lifetime parameters (needed for &[T] borrowed slice fields).
1035 // Allow type parameters on unit-only enums (all variants are unit) — the
1036 // companion struct has no field columns to type-parameterise, and the three
1037 // unit-enum impls (UnitEnumFactor, IntoR, IntoList) handle generics via the
1038 // split path in enum_expansion.rs.
1039 // Reject type and const parameters for everything else.
1040 let has_type_params = input.generics.type_params().next().is_some();
1041 let has_const_params = input.generics.const_params().next().is_some();
1042 if has_type_params || has_const_params {
1043 let is_unit_only_enum = matches!(&input.data, Data::Enum(e)
1044 if e.variants.iter().all(|v| matches!(v.fields, Fields::Unit)));
1045 if !is_unit_only_enum {
1046 return Err(syn::Error::new_spanned(
1047 &input.generics,
1048 "DataFrameRow does not support type or const generic parameters",
1049 ));
1050 }
1051 }
1052
1053 // Parse attributes
1054 let attrs = parse_dataframe_attrs(&input)?;
1055
1056 let df_name = attrs
1057 .name
1058 .clone()
1059 .unwrap_or_else(|| format_ident!("{}DataFrame", row_name));
1060
1061 let base = match &input.data {
1062 Data::Struct(data) => {
1063 // `align` is a no-op on structs (only semantically meaningful for enums)
1064 derive_struct_dataframe(row_name, &input, data, &df_name, &attrs)
1065 }
1066 Data::Enum(data) => {
1067 // align is implicit for enums — accept but don't require
1068 derive_enum_dataframe(row_name, &input, data, &df_name, &attrs)
1069 }
1070 Data::Union(_) => Err(syn::Error::new_spanned(
1071 row_name,
1072 "DataFrameRow does not support unions",
1073 )),
1074 }?;
1075
1076 // Generate IntoR for the companion DataFrame type so it can be returned
1077 // directly from #[miniextendr] functions. This ensures both the standalone
1078 // #[derive(DataFrameRow)] path and the #[miniextendr(dataframe)] path
1079 // produce identical output.
1080 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1081 Ok(quote::quote! {
1082 #base
1083
1084 impl #impl_generics ::miniextendr_api::into_r::IntoR for #df_name #ty_generics #where_clause {
1085 type Error = std::convert::Infallible;
1086
1087 #[inline]
1088 fn try_into_sexp(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
1089 Ok(self.into_sexp())
1090 }
1091
1092 #[inline]
1093 unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
1094 self.try_into_sexp()
1095 }
1096
1097 #[inline]
1098 fn into_sexp(self) -> ::miniextendr_api::SEXP {
1099 ::miniextendr_api::convert::ColumnSource::into_column_list(self).into_sexp()
1100 }
1101
1102 #[inline]
1103 unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::SEXP {
1104 ::miniextendr_api::convert::ColumnSource::into_column_list(self).into_sexp()
1105 }
1106 }
1107 })
1108}
1109// endregion
1110
1111// region: Struct path (existing logic, extracted)
1112
1113/// Generate `DataFrameRow` expansion for struct types.
1114///
1115/// Produces:
1116/// - A companion struct `{Name}DataFrame` with `Vec<T>` columns
1117/// - `impl IntoDataFrame for {Name}DataFrame`
1118/// - `impl From<Vec<{Name}>> for {Name}DataFrame`
1119/// - `impl IntoIterator` (for named structs without expansion)
1120/// - Associated methods: `to_dataframe`, `from_dataframe`, `from_rows`, `from_rows_par`
1121/// - A compile-time `IntoList` assertion (for non-expanded named structs)
1122///
1123/// Handles fixed-array expansion (`[T; N]`), pinned-width Vec expansion
1124/// (`Vec<T>` + `width`), and auto-expand Vec (`Vec<T>` + `expand`).
1125fn derive_struct_dataframe(
1126 row_name: &syn::Ident,
1127 input: &DeriveInput,
1128 data: &syn::DataStruct,
1129 df_name: &syn::Ident,
1130 attrs: &DataFrameAttrs,
1131) -> syn::Result<TokenStream> {
1132 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1133
1134 let is_tuple_struct = matches!(&data.fields, Fields::Unnamed(_));
1135 let is_unit_struct = matches!(&data.fields, Fields::Unit);
1136
1137 // Resolve fields through the new FieldAttrs + type classification system.
1138 let resolved: Vec<ResolvedField> = match &data.fields {
1139 Fields::Named(fields) => {
1140 let mut out = Vec::new();
1141 for (i, f) in fields.named.iter().enumerate() {
1142 if let Some(rf) = resolve_struct_field(f, i, false)? {
1143 out.push(rf);
1144 }
1145 }
1146 out
1147 }
1148 Fields::Unnamed(fields) => {
1149 let mut out = Vec::new();
1150 for (i, f) in fields.unnamed.iter().enumerate() {
1151 if let Some(rf) = resolve_struct_field(f, i, true)? {
1152 out.push(rf);
1153 }
1154 }
1155 out
1156 }
1157 Fields::Unit => vec![],
1158 };
1159
1160 // Check whether any field uses expansion — affects whether we can generate
1161 // IntoIterator (expanded fields change the companion struct shape).
1162 let has_expansion = resolved
1163 .iter()
1164 .any(|rf| !matches!(rf, ResolvedField::Single(..)));
1165 // Track which Rust fields were skipped (for destructure patterns).
1166 let skipped_fields: Vec<syn::Ident> = match &data.fields {
1167 Fields::Named(fields) => fields
1168 .named
1169 .iter()
1170 .filter_map(|f| {
1171 let fa = parse_field_attrs(f).ok()?;
1172 if fa.skip {
1173 Some(f.ident.as_ref().unwrap().clone())
1174 } else {
1175 None
1176 }
1177 })
1178 .collect(),
1179 _ => vec![],
1180 };
1181
1182 let has_tag = attrs.tag.is_some();
1183 let row_name_str = row_name.to_string();
1184
1185 // region: Build flat column lists from resolved fields
1186 // Each resolved field may produce 1..N columns.
1187 struct FlatCol {
1188 /// Companion struct field name.
1189 df_field: syn::Ident,
1190 /// Column name string in the R data frame.
1191 col_name_str: String,
1192 /// Type of the companion Vec<T>.
1193 vec_elem_ty: syn::Type,
1194 /// `#[dataframe(as_list)]` on a struct-typed field — companion stores
1195 /// `Vec<List>`. The `from_rows_par` pre-pass handles these sequentially
1196 /// instead of scatter-writing (List doesn't implement Default).
1197 needs_into_list: bool,
1198 }
1199
1200 let mut flat_cols: Vec<FlatCol> = Vec::new();
1201
1202 for rf in &resolved {
1203 match rf {
1204 ResolvedField::Single(data) => {
1205 flat_cols.push(FlatCol {
1206 df_field: data.col_name.clone(),
1207 col_name_str: data.col_name_str.clone(),
1208 vec_elem_ty: data.ty.clone(),
1209 needs_into_list: data.needs_into_list,
1210 });
1211 }
1212 ResolvedField::ExpandedFixed(data) => {
1213 for i in 1..=data.len {
1214 let name = format!("{}_{}", data.base_name, i);
1215 flat_cols.push(FlatCol {
1216 df_field: format_ident!("{}_{}", data.base_name, i),
1217 col_name_str: name,
1218 vec_elem_ty: data.elem_ty.clone(),
1219 needs_into_list: false,
1220 });
1221 }
1222 }
1223 ResolvedField::ExpandedVec(data) => {
1224 for i in 1..=data.width {
1225 let name = format!("{}_{}", data.base_name, i);
1226 let elem_ty = &data.elem_ty;
1227 let opt_ty: syn::Type = syn::parse_quote!(Option<#elem_ty>);
1228 flat_cols.push(FlatCol {
1229 df_field: format_ident!("{}_{}", data.base_name, i),
1230 col_name_str: name,
1231 vec_elem_ty: opt_ty,
1232 needs_into_list: false,
1233 });
1234 }
1235 }
1236 // AutoExpandVec / Struct do not produce FlatCols — handled separately.
1237 ResolvedField::AutoExpandVec(..) | ResolvedField::Struct(..) => {}
1238 // Map (#919): two parallel list-columns `<base>_keys` / `<base>_values`.
1239 // Each is a `Vec<Vec<K>>` / `Vec<Vec<V>>` (VECSXP of typed vectors).
1240 ResolvedField::Map(data) => {
1241 let keys_col_name = format!("{}_keys", data.base_name);
1242 let vals_col_name = format!("{}_values", data.base_name);
1243 let key_ty = &data.key_ty;
1244 let val_ty = &data.val_ty;
1245 let keys_vec_ty: syn::Type = syn::parse_quote!(Vec<#key_ty>);
1246 let vals_vec_ty: syn::Type = syn::parse_quote!(Vec<#val_ty>);
1247 flat_cols.push(FlatCol {
1248 df_field: format_ident!("{}_keys", data.base_name),
1249 col_name_str: keys_col_name,
1250 vec_elem_ty: keys_vec_ty,
1251 needs_into_list: false,
1252 });
1253 flat_cols.push(FlatCol {
1254 df_field: format_ident!("{}_values", data.base_name),
1255 col_name_str: vals_col_name,
1256 vec_elem_ty: vals_vec_ty,
1257 needs_into_list: false,
1258 });
1259 }
1260 }
1261 }
1262 // endregion
1263
1264 // region: Collect auto-expand fields
1265 struct AutoExpandCol {
1266 /// Companion struct field name.
1267 df_field: syn::Ident,
1268 /// Container type (Vec<T> or Box<[T]>).
1269 container_ty: syn::Type,
1270 }
1271
1272 let auto_expand_cols: Vec<AutoExpandCol> = resolved
1273 .iter()
1274 .filter_map(|rf| {
1275 if let ResolvedField::AutoExpandVec(data) = rf {
1276 Some(AutoExpandCol {
1277 df_field: format_ident!("{}", data.col_name_str),
1278 container_ty: data.container_ty.clone(),
1279 })
1280 } else {
1281 None
1282 }
1283 })
1284 .collect();
1285 let has_auto_expand = !auto_expand_cols.is_empty();
1286 // endregion
1287
1288 // region: Collect struct (DataFrameRow-flattened) fields (#485)
1289 //
1290 // Only the codegen-time bits are mirrored here — `rust_name` / `tuple_index`
1291 // are read directly off `ResolvedField::Struct` at the per-row pushes site.
1292 struct StructCol {
1293 df_field: syn::Ident,
1294 col_name_str: String,
1295 inner_ty: syn::Type,
1296 }
1297
1298 let struct_cols: Vec<StructCol> = resolved
1299 .iter()
1300 .filter_map(|rf| {
1301 if let ResolvedField::Struct(data) = rf {
1302 Some(StructCol {
1303 df_field: data.col_name.clone(),
1304 col_name_str: data.col_name_str.clone(),
1305 inner_ty: data.inner_ty.clone(),
1306 })
1307 } else {
1308 None
1309 }
1310 })
1311 .collect();
1312 let has_struct = !struct_cols.is_empty();
1313
1314 // Any `#[dataframe(as_list)]` on a struct-typed field stores `List` in the
1315 // companion (#485 opt-out). We can't round-trip List back to the inner
1316 // struct without a `FromList`-like trait, and `List` doesn't impl
1317 // `Default`, so several codegen branches need to suppress themselves:
1318 // IntoIterator generation, the `IntoList` compile-time assertion, and
1319 // `from_rows_par`.
1320 let has_into_list_struct = resolved
1321 .iter()
1322 .any(|rf| matches!(rf, ResolvedField::Single(d) if d.needs_into_list));
1323 // endregion
1324
1325 // region: Companion struct
1326 let tag_field_decl = if has_tag {
1327 quote! { pub _tag: Vec<String>, }
1328 } else {
1329 TokenStream::new()
1330 };
1331
1332 let mut df_fields_tokens: Vec<TokenStream> = flat_cols
1333 .iter()
1334 .map(|fc| {
1335 let name = &fc.df_field;
1336 let ty = &fc.vec_elem_ty;
1337 quote! { pub #name: Vec<#ty> }
1338 })
1339 .collect();
1340 for ac in &auto_expand_cols {
1341 let name = &ac.df_field;
1342 let cty = &ac.container_ty;
1343 df_fields_tokens.push(quote! { pub #name: Vec<#cty> });
1344 }
1345 for sc in &struct_cols {
1346 let name = &sc.df_field;
1347 let ity = &sc.inner_ty;
1348 df_fields_tokens.push(quote! { pub #name: Vec<#ity> });
1349 }
1350
1351 let len_field_decl = if flat_cols.is_empty()
1352 && auto_expand_cols.is_empty()
1353 && struct_cols.is_empty()
1354 && !has_tag
1355 {
1356 quote! { pub _len: usize, }
1357 } else {
1358 TokenStream::new()
1359 };
1360
1361 let dataframe_struct = quote! {
1362 #[derive(Debug, Clone)]
1363 pub struct #df_name #impl_generics #where_clause {
1364 #tag_field_decl
1365 #len_field_decl
1366 #(#df_fields_tokens),*
1367 }
1368 };
1369 // endregion
1370
1371 // region: IntoDataFrame
1372 let length_ref = if has_tag {
1373 quote! { self._tag.len() }
1374 } else if !flat_cols.is_empty() {
1375 let first = &flat_cols[0].df_field;
1376 quote! { self.#first.len() }
1377 } else if !auto_expand_cols.is_empty() {
1378 let first = &auto_expand_cols[0].df_field;
1379 quote! { self.#first.len() }
1380 } else if !struct_cols.is_empty() {
1381 let first = &struct_cols[0].df_field;
1382 quote! { self.#first.len() }
1383 } else {
1384 quote! { self._len }
1385 };
1386
1387 // Each pair protects its SEXP via `__scope.protect_raw` so previously-built
1388 // column SEXPs survive subsequent column allocations. Pre-fix the raw
1389 // `vec![(name, into_sexp(...)), ...]` left every SEXP unrooted across the
1390 // next column's allocations — UAF under gctorture
1391 // (reviews/2026-05-07-gctorture-audit.md).
1392 let tag_pair = if let Some(ref tag_name) = attrs.tag {
1393 quote! { (#tag_name, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self._tag))), }
1394 } else {
1395 TokenStream::new()
1396 };
1397
1398 let df_pairs: Vec<TokenStream> = flat_cols
1399 .iter()
1400 .map(|fc| {
1401 let name = &fc.df_field;
1402 let name_str = &fc.col_name_str;
1403 quote! { (#name_str, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self.#name))) }
1404 })
1405 .collect();
1406
1407 let mut length_checks: Vec<TokenStream> = flat_cols
1408 .iter()
1409 .map(|fc| {
1410 let name = &fc.df_field;
1411 let name_str = &fc.col_name_str;
1412 quote! {
1413 assert!(
1414 self.#name.len() == _n_rows,
1415 "column length mismatch in {}: column `{}` has length {} but expected {}",
1416 stringify!(#df_name),
1417 #name_str,
1418 self.#name.len(),
1419 _n_rows,
1420 );
1421 }
1422 })
1423 .collect();
1424 for sc in &struct_cols {
1425 let name = &sc.df_field;
1426 let name_str = &sc.col_name_str;
1427 length_checks.push(quote! {
1428 assert!(
1429 self.#name.len() == _n_rows,
1430 "column length mismatch in {}: struct column `{}` has length {} but expected {}",
1431 stringify!(#df_name),
1432 #name_str,
1433 self.#name.len(),
1434 _n_rows,
1435 );
1436 });
1437 }
1438
1439 let into_dataframe_impl = if has_auto_expand || has_struct {
1440 // Dynamic pair building: iterate resolved fields in order,
1441 // emitting static pairs for flat columns and runtime-expanded
1442 // pairs for auto-expand fields.
1443 let tag_push_pair = if let Some(ref tag_name) = attrs.tag {
1444 quote! {
1445 __df_pairs.push((#tag_name.to_string(), __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self._tag))));
1446 }
1447 } else {
1448 TokenStream::new()
1449 };
1450
1451 let pair_pushes: Vec<TokenStream> = resolved
1452 .iter()
1453 .map(|rf| match rf {
1454 ResolvedField::Single(data) => {
1455 let col_name = &data.col_name;
1456 let col_name_str = &data.col_name_str;
1457 quote! {
1458 __df_pairs.push((
1459 #col_name_str.to_string(),
1460 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self.#col_name)),
1461 ));
1462 }
1463 }
1464 ResolvedField::ExpandedFixed(data) => {
1465 let pushes: Vec<TokenStream> = (1..=data.len)
1466 .map(|i| {
1467 let name = format!("{}_{}", data.base_name, i);
1468 let ident = format_ident!("{}_{}", data.base_name, i);
1469 quote! {
1470 __df_pairs.push((
1471 #name.to_string(),
1472 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self.#ident)),
1473 ));
1474 }
1475 })
1476 .collect();
1477 quote! { #(#pushes)* }
1478 }
1479 ResolvedField::ExpandedVec(data) => {
1480 let pushes: Vec<TokenStream> = (1..=data.width)
1481 .map(|i| {
1482 let name = format!("{}_{}", data.base_name, i);
1483 let ident = format_ident!("{}_{}", data.base_name, i);
1484 quote! {
1485 __df_pairs.push((
1486 #name.to_string(),
1487 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self.#ident)),
1488 ));
1489 }
1490 })
1491 .collect();
1492 quote! { #(#pushes)* }
1493 }
1494 ResolvedField::AutoExpandVec(data) => {
1495 let col_name = &data.col_name;
1496 let col_name_str = &data.col_name_str;
1497 let elem_ty = &data.elem_ty;
1498 quote! {
1499 {
1500 let __auto = self.#col_name;
1501 let __max = __auto.iter().map(|v| v.len()).max().unwrap_or(0);
1502 let mut __cols: Vec<Vec<Option<#elem_ty>>> = (0..__max)
1503 .map(|_| Vec::with_capacity(_n_rows))
1504 .collect();
1505 for __row_vec in &__auto {
1506 for (__i, __col) in __cols.iter_mut().enumerate() {
1507 __col.push(__row_vec.get(__i).cloned());
1508 }
1509 }
1510 for (__i, __col) in __cols.into_iter().enumerate() {
1511 __df_pairs.push((
1512 format!("{}_{}", #col_name_str, __i + 1),
1513 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(__col)),
1514 ));
1515 }
1516 }
1517 }
1518 }
1519 ResolvedField::Struct(data) => {
1520 // Issue #485: convert `Vec<Inner>` via Inner::to_dataframe,
1521 // extract its named columns, and push under `<base>_` prefix.
1522 let col_name = &data.col_name;
1523 let base_name_str = &data.col_name_str;
1524 let inner_ty = &data.inner_ty;
1525 quote! {
1526 {
1527 let __inner_df = <#inner_ty>::to_dataframe(self.#col_name);
1528 let __inner_cols = ::miniextendr_api::convert::ColumnSource::into_named_columns(__inner_df);
1529 for (__inner_col_name, __inner_col_sexp) in __inner_cols {
1530 // Protect the source column SEXP across subsequent allocations.
1531 let __src = __scope.protect_raw(__inner_col_sexp);
1532 __df_pairs.push((
1533 format!("{}_{}", #base_name_str, __inner_col_name),
1534 __src,
1535 ));
1536 }
1537 }
1538 }
1539 }
1540 // Map (#919): push two list-columns `<base>_keys` / `<base>_values`.
1541 ResolvedField::Map(data) => {
1542 let keys_ident = format_ident!("{}_keys", data.base_name);
1543 let vals_ident = format_ident!("{}_values", data.base_name);
1544 let keys_name = format!("{}_keys", data.base_name);
1545 let vals_name = format!("{}_values", data.base_name);
1546 quote! {
1547 __df_pairs.push((
1548 #keys_name.to_string(),
1549 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self.#keys_ident)),
1550 ));
1551 __df_pairs.push((
1552 #vals_name.to_string(),
1553 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self.#vals_ident)),
1554 ));
1555 }
1556 }
1557 })
1558 .collect();
1559
1560 quote! {
1561 impl #impl_generics ::miniextendr_api::convert::ColumnSource for #df_name #ty_generics #where_clause {
1562 fn into_column_list(self) -> ::miniextendr_api::List {
1563 let _n_rows = #length_ref;
1564 #(#length_checks)*
1565 // SAFETY: into_column_list only runs on the R main thread.
1566 // ProtectScope keeps each column SEXP rooted across the
1567 // next column's allocations; from_raw_pairs writes them
1568 // into the parent VECSXP before we drop the scope.
1569 unsafe {
1570 let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
1571 let mut __df_pairs: Vec<(
1572 String,
1573 ::miniextendr_api::SEXP,
1574 )> = Vec::new();
1575 #tag_push_pair
1576 #(#pair_pushes)*
1577 ::miniextendr_api::list::List::from_raw_pairs(__df_pairs)
1578 .set_class_str(&["data.frame"])
1579 .set_row_names_int(_n_rows)
1580 }
1581 }
1582 }
1583 }
1584 } else {
1585 quote! {
1586 impl #impl_generics ::miniextendr_api::convert::ColumnSource for #df_name #ty_generics #where_clause {
1587 fn into_column_list(self) -> ::miniextendr_api::List {
1588 let _n_rows = #length_ref;
1589 #(#length_checks)*
1590 // SAFETY: see auto-expand branch.
1591 unsafe {
1592 let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
1593 ::miniextendr_api::list::List::from_raw_pairs(vec![
1594 #tag_pair
1595 #(#df_pairs),*
1596 ])
1597 .set_class_str(&["data.frame"])
1598 .set_row_names_int(_n_rows)
1599 }
1600 }
1601 }
1602 }
1603 };
1604 // endregion
1605
1606 // region: From<Vec<RowType>>
1607 let mut col_vec_inits: Vec<TokenStream> = flat_cols
1608 .iter()
1609 .map(|fc| {
1610 let name = &fc.df_field;
1611 let ty = &fc.vec_elem_ty;
1612 quote! { let mut #name: Vec<#ty> = Vec::with_capacity(len); }
1613 })
1614 .collect();
1615 for ac in &auto_expand_cols {
1616 let name = &ac.df_field;
1617 let cty = &ac.container_ty;
1618 col_vec_inits.push(quote! { let mut #name: Vec<#cty> = Vec::with_capacity(len); });
1619 }
1620 for sc in &struct_cols {
1621 let name = &sc.df_field;
1622 let ity = &sc.inner_ty;
1623 col_vec_inits.push(quote! { let mut #name: Vec<#ity> = Vec::with_capacity(len); });
1624 }
1625
1626 let tag_init = if has_tag {
1627 quote! { let mut _tag: Vec<String> = Vec::with_capacity(len); }
1628 } else {
1629 TokenStream::new()
1630 };
1631
1632 let tag_push = if has_tag {
1633 quote! { _tag.push(#row_name_str.to_string()); }
1634 } else {
1635 TokenStream::new()
1636 };
1637
1638 // Generate push statements for each resolved field
1639 let col_pushes: Vec<TokenStream> = resolved
1640 .iter()
1641 .map(|rf| match rf {
1642 ResolvedField::Single(data) => {
1643 let access = if let Some(idx) = &data.tuple_index {
1644 quote! { row.#idx }
1645 } else {
1646 let rust_name = &data.rust_name;
1647 quote! { row.#rust_name }
1648 };
1649 let col_name = &data.col_name;
1650 if data.needs_into_list {
1651 quote! { #col_name.push(::miniextendr_api::list::IntoList::into_list(#access)); }
1652 } else {
1653 quote! { #col_name.push(#access); }
1654 }
1655 }
1656 ResolvedField::ExpandedFixed(data) => {
1657 let access = if let Some(idx) = &data.tuple_index {
1658 quote! { row.#idx }
1659 } else {
1660 let rust_name = &data.rust_name;
1661 quote! { row.#rust_name }
1662 };
1663 let bind = format_ident!("__arr_{}", data.rust_name);
1664 let pushes: Vec<TokenStream> = (0..data.len)
1665 .map(|i| {
1666 let col_ident = format_ident!("{}_{}", data.base_name, i + 1);
1667 let idx = syn::Index::from(i);
1668 quote! { #col_ident.push(#bind[#idx]); }
1669 })
1670 .collect();
1671 quote! {
1672 let #bind = #access;
1673 #(#pushes)*
1674 }
1675 }
1676 ResolvedField::ExpandedVec(data) => {
1677 let access = if let Some(idx) = &data.tuple_index {
1678 quote! { row.#idx }
1679 } else {
1680 let rust_name = &data.rust_name;
1681 quote! { row.#rust_name }
1682 };
1683 let bind = format_ident!("__vec_{}", data.rust_name);
1684 let pushes: Vec<TokenStream> = (0..data.width)
1685 .map(|i| {
1686 let col_ident = format_ident!("{}_{}", data.base_name, i + 1);
1687 quote! { #col_ident.push(#bind.get(#i).cloned()); }
1688 })
1689 .collect();
1690 quote! {
1691 let #bind = #access;
1692 #(#pushes)*
1693 }
1694 }
1695 ResolvedField::AutoExpandVec(data) => {
1696 let access = if let Some(idx) = &data.tuple_index {
1697 quote! { row.#idx }
1698 } else {
1699 let rust_name = &data.rust_name;
1700 quote! { row.#rust_name }
1701 };
1702 let col_name = &data.col_name;
1703 quote! { #col_name.push(#access); }
1704 }
1705 ResolvedField::Struct(data) => {
1706 let access = if let Some(idx) = &data.tuple_index {
1707 quote! { row.#idx }
1708 } else {
1709 let rust_name = &data.rust_name;
1710 quote! { row.#rust_name }
1711 };
1712 let col_name = &data.col_name;
1713 quote! { #col_name.push(#access); }
1714 }
1715 // Map (#919): unzip into parallel keys/values vecs.
1716 ResolvedField::Map(data) => {
1717 let access = if let Some(idx) = &data.tuple_index {
1718 quote! { row.#idx }
1719 } else {
1720 let rust_name = &data.rust_name;
1721 quote! { row.#rust_name }
1722 };
1723 let keys_col = format_ident!("{}_keys", data.base_name);
1724 let vals_col = format_ident!("{}_values", data.base_name);
1725 quote! {
1726 let (__mx_keys, __mx_vals) = #access
1727 .into_iter()
1728 .unzip::<_, _, Vec<_>, Vec<_>>();
1729 #keys_col.push(__mx_keys);
1730 #vals_col.push(__mx_vals);
1731 }
1732 }
1733 })
1734 .collect();
1735
1736 let tag_struct_field = if has_tag {
1737 quote! { _tag, }
1738 } else {
1739 TokenStream::new()
1740 };
1741
1742 let len_struct_field = if flat_cols.is_empty()
1743 && auto_expand_cols.is_empty()
1744 && struct_cols.is_empty()
1745 && !has_tag
1746 {
1747 quote! { _len: len, }
1748 } else {
1749 TokenStream::new()
1750 };
1751
1752 let mut col_struct_fields: Vec<TokenStream> = flat_cols
1753 .iter()
1754 .map(|fc| {
1755 let name = &fc.df_field;
1756 quote! { #name }
1757 })
1758 .collect();
1759 for ac in &auto_expand_cols {
1760 let name = &ac.df_field;
1761 col_struct_fields.push(quote! { #name });
1762 }
1763 for sc in &struct_cols {
1764 let name = &sc.df_field;
1765 col_struct_fields.push(quote! { #name });
1766 }
1767
1768 // For skipped fields in destructure: bind to `_`
1769 let skip_bindings: Vec<TokenStream> = skipped_fields
1770 .iter()
1771 .map(|name| quote! { let _ = row.#name; })
1772 .collect();
1773
1774 let from_vec_impl = quote! {
1775 impl #impl_generics From<Vec<#row_name #ty_generics>> for #df_name #ty_generics #where_clause {
1776 fn from(rows: Vec<#row_name #ty_generics>) -> Self {
1777 let len = rows.len();
1778 #tag_init
1779 #(#col_vec_inits)*
1780 for row in rows {
1781 #tag_push
1782 #(#skip_bindings)*
1783 #(#col_pushes)*
1784 }
1785 #df_name {
1786 #tag_struct_field
1787 #len_struct_field
1788 #(#col_struct_fields),*
1789 }
1790 }
1791 }
1792 };
1793 // endregion
1794
1795 // region: Generate from_rows_par (parallel scatter-write via ColumnWriter)
1796 //
1797 // Two field kinds require special handling instead of parallel scatter-write:
1798 // - struct (DataFrameRow-flattened) fields (#485): companion stores
1799 // `Vec<Inner>` where `Inner` doesn't implement `Default`. These are
1800 // collected sequentially in a pre-pass (`for __prerow in &rows { ... }`)
1801 // before `into_par_iter()` consumes the vector. Requires `Inner: Clone`.
1802 // - `as_list`-on-struct fields (#485 opt-out) store `Vec<List>` in the
1803 // companion, and `List` doesn't implement `Default`. Same pre-pass approach.
1804 // Both are handled via sequential pre-pass + skip in the parallel loop.
1805 // The pre-pass is O(n) extra per struct/list-struct field but does not change
1806 // asymptotic complexity — just adds a constant factor for these column types.
1807 let from_rows_par_method = if !flat_cols.is_empty()
1808 || !auto_expand_cols.is_empty()
1809 || has_tag
1810 || has_struct
1811 || has_into_list_struct
1812 {
1813 // Column declarations:
1814 // - scalar / expand cols: vec![default; len] (scatter-write in parallel)
1815 // - struct / as_list-struct cols: Vec::with_capacity(len) filled in pre-pass
1816 let mut par_col_decls = Vec::new();
1817 if has_tag {
1818 par_col_decls.push(quote! {
1819 let mut _tag: Vec<String> = vec![String::new(); len];
1820 });
1821 }
1822 // Sequential pre-pass: struct fields (Inner: Clone required).
1823 // Iterate resolved to pick up tuple_index for tuple-struct outers.
1824 for rf in &resolved {
1825 if let ResolvedField::Struct(data) = rf {
1826 let col_name = &data.col_name;
1827 let ity = &data.inner_ty;
1828 let access = if let Some(idx) = &data.tuple_index {
1829 quote! { __prerow.#idx }
1830 } else {
1831 let rust_name = &data.rust_name;
1832 quote! { __prerow.#rust_name }
1833 };
1834 par_col_decls.push(quote! {
1835 let mut #col_name: Vec<#ity> = Vec::with_capacity(len);
1836 for __prerow in &rows {
1837 #col_name.push(::core::clone::Clone::clone(&#access));
1838 }
1839 });
1840 }
1841 }
1842 // Sequential pre-pass: as_list-on-struct fields (List: !Default).
1843 for rf in &resolved {
1844 if let ResolvedField::Single(data) = rf
1845 && data.needs_into_list
1846 {
1847 let col_name = &data.col_name;
1848 let rust_name = &data.rust_name;
1849 let access = if let Some(idx) = &data.tuple_index {
1850 quote! { __prerow.#idx }
1851 } else {
1852 quote! { __prerow.#rust_name }
1853 };
1854 par_col_decls.push(quote! {
1855 let mut #col_name: Vec<::miniextendr_api::list::List> = Vec::with_capacity(len);
1856 for __prerow in &rows {
1857 #col_name.push(::miniextendr_api::list::IntoList::into_list(
1858 ::core::clone::Clone::clone(&#access)
1859 ));
1860 }
1861 });
1862 }
1863 }
1864 // Parallel scalar/expand columns.
1865 for fc in &flat_cols {
1866 if fc.needs_into_list {
1867 // Handled in the sequential pre-pass above.
1868 continue;
1869 }
1870 let name = &fc.df_field;
1871 let ty = &fc.vec_elem_ty;
1872 par_col_decls.push(quote! {
1873 let mut #name: Vec<#ty> = vec![<#ty as ::core::default::Default>::default(); len];
1874 });
1875 }
1876 for ac in &auto_expand_cols {
1877 let name = &ac.df_field;
1878 let cty = &ac.container_ty;
1879 par_col_decls.push(quote! {
1880 let mut #name: Vec<#cty> = vec![<#cty as ::core::default::Default>::default(); len];
1881 });
1882 }
1883
1884 // Writer declarations (only for scatter-write cols — struct/as_list pre-pass
1885 // cols are already populated and need no ColumnWriter).
1886 let mut writer_decls = Vec::new();
1887 if has_tag {
1888 writer_decls.push(quote! {
1889 let __w_tag = unsafe {
1890 ::miniextendr_api::rayon_bridge::ColumnWriter::new(&mut _tag)
1891 };
1892 });
1893 }
1894 for fc in &flat_cols {
1895 if fc.needs_into_list {
1896 continue;
1897 }
1898 let name = &fc.df_field;
1899 let w_name = format_ident!("__w_{}", name);
1900 writer_decls.push(quote! {
1901 let #w_name = unsafe {
1902 ::miniextendr_api::rayon_bridge::ColumnWriter::new(&mut #name)
1903 };
1904 });
1905 }
1906 for ac in &auto_expand_cols {
1907 let name = &ac.df_field;
1908 let w_name = format_ident!("__w_{}", name);
1909 writer_decls.push(quote! {
1910 let #w_name = unsafe {
1911 ::miniextendr_api::rayon_bridge::ColumnWriter::new(&mut #name)
1912 };
1913 });
1914 }
1915
1916 // Write calls per resolved field (parallel scatter-write only).
1917 let tag_write = if has_tag {
1918 quote! { __w_tag.write(__i, #row_name_str.to_string()); }
1919 } else {
1920 TokenStream::new()
1921 };
1922
1923 let par_write_calls: Vec<TokenStream> = resolved
1924 .iter()
1925 .map(|rf| match rf {
1926 ResolvedField::Single(data) => {
1927 if data.needs_into_list {
1928 // Handled in the sequential pre-pass; skip in par loop.
1929 return TokenStream::new();
1930 }
1931 let access = if let Some(idx) = &data.tuple_index {
1932 quote! { __row.#idx }
1933 } else {
1934 let rust_name = &data.rust_name;
1935 quote! { __row.#rust_name }
1936 };
1937 let w_name = format_ident!("__w_{}", data.col_name);
1938 quote! { #w_name.write(__i, #access); }
1939 }
1940 ResolvedField::ExpandedFixed(data) => {
1941 let access = if let Some(idx) = &data.tuple_index {
1942 quote! { __row.#idx }
1943 } else {
1944 let rust_name = &data.rust_name;
1945 quote! { __row.#rust_name }
1946 };
1947 let bind = format_ident!("__arr_{}", data.rust_name);
1948 let writes: Vec<TokenStream> = (0..data.len)
1949 .map(|i| {
1950 let w_name = format_ident!("__w_{}_{}", data.base_name, i + 1);
1951 let idx = syn::Index::from(i);
1952 quote! { #w_name.write(__i, #bind[#idx]); }
1953 })
1954 .collect();
1955 quote! {
1956 let #bind = #access;
1957 #(#writes)*
1958 }
1959 }
1960 ResolvedField::ExpandedVec(data) => {
1961 let access = if let Some(idx) = &data.tuple_index {
1962 quote! { __row.#idx }
1963 } else {
1964 let rust_name = &data.rust_name;
1965 quote! { __row.#rust_name }
1966 };
1967 let bind = format_ident!("__vec_{}", data.rust_name);
1968 let writes: Vec<TokenStream> = (0..data.width)
1969 .map(|i| {
1970 let w_name = format_ident!("__w_{}_{}", data.base_name, i + 1);
1971 quote! { #w_name.write(__i, #bind.get(#i).cloned()); }
1972 })
1973 .collect();
1974 quote! {
1975 let #bind = #access;
1976 #(#writes)*
1977 }
1978 }
1979 ResolvedField::AutoExpandVec(data) => {
1980 let access = if let Some(idx) = &data.tuple_index {
1981 quote! { __row.#idx }
1982 } else {
1983 let rust_name = &data.rust_name;
1984 quote! { __row.#rust_name }
1985 };
1986 let w_name = format_ident!("__w_{}", data.col_name);
1987 quote! { #w_name.write(__i, #access); }
1988 }
1989 // Struct fields (#485) are collected in the sequential pre-pass
1990 // above; nothing to write in the parallel loop.
1991 ResolvedField::Struct(_) => TokenStream::new(),
1992 // Map (#919): unzip into parallel keys/values vecs via scatter-write.
1993 ResolvedField::Map(data) => {
1994 let access = if let Some(idx) = &data.tuple_index {
1995 quote! { __row.#idx }
1996 } else {
1997 let rust_name = &data.rust_name;
1998 quote! { __row.#rust_name }
1999 };
2000 let w_keys = format_ident!("__w_{}_keys", data.base_name);
2001 let w_vals = format_ident!("__w_{}_values", data.base_name);
2002 quote! {
2003 let (__mx_keys, __mx_vals) = #access
2004 .into_iter()
2005 .unzip::<_, _, Vec<_>, Vec<_>>();
2006 #w_keys.write(__i, __mx_keys);
2007 #w_vals.write(__i, __mx_vals);
2008 }
2009 }
2010 })
2011 .collect();
2012
2013 let par_skip_bindings: Vec<TokenStream> = skipped_fields
2014 .iter()
2015 .map(|name| quote! { let _ = __row.#name; })
2016 .collect();
2017
2018 // Return struct fields
2019 let par_tag_field = if has_tag {
2020 quote! { _tag, }
2021 } else {
2022 TokenStream::new()
2023 };
2024 // Emit `_len: len` only when the companion struct has a `_len` field —
2025 // that is, when there are truly no column vecs at all (no scalars, no
2026 // as_list-on-struct fields, no struct-flattened fields, no tag).
2027 // `as_list`-on-struct fields live in `flat_cols` with `needs_into_list=true`;
2028 // they provide their own length reference and do NOT require `_len`.
2029 // The `flat_cols.iter().all(…)` guard is redundant with `flat_cols.is_empty()`
2030 // but makes the intent explicit: _len is emitted only when every dimension
2031 // that tracks length is absent.
2032 let par_len_field = if flat_cols.is_empty()
2033 && flat_cols.iter().all(|fc| !fc.needs_into_list)
2034 && auto_expand_cols.is_empty()
2035 && !has_tag
2036 && struct_cols.is_empty()
2037 {
2038 quote! { _len: len, }
2039 } else {
2040 TokenStream::new()
2041 };
2042 let mut par_struct_fields: Vec<TokenStream> = flat_cols
2043 .iter()
2044 .map(|fc| {
2045 let name = &fc.df_field;
2046 quote! { #name }
2047 })
2048 .collect();
2049 for ac in &auto_expand_cols {
2050 let name = &ac.df_field;
2051 par_struct_fields.push(quote! { #name });
2052 }
2053 for sc in &struct_cols {
2054 let name = &sc.df_field;
2055 par_struct_fields.push(quote! { #name });
2056 }
2057
2058 // Only emit an into_par_iter call when there are scalar/expand/tag cols
2059 // to scatter-write; struct/as_list-only structs skip the parallel loop.
2060 let has_par_cols = !flat_cols.iter().all(|fc| fc.needs_into_list)
2061 || !auto_expand_cols.is_empty()
2062 || has_tag;
2063 let par_loop = if has_par_cols {
2064 quote! {
2065 {
2066 ::miniextendr_api::optionals::parallel::ensure_pool();
2067 #(#writer_decls)*
2068 rows.into_par_iter().enumerate().for_each(|(__i, __row)| unsafe {
2069 #tag_write
2070 #(#par_write_calls)*
2071 #(#par_skip_bindings)*
2072 });
2073 }
2074 }
2075 } else {
2076 // All columns were collected in the pre-pass; rows already consumed.
2077 quote! { let _rows = rows; }
2078 };
2079
2080 // Build `where Inner: Clone` bounds for all struct-flattened fields.
2081 // Emitting these on the method (rather than in a `const _` assertion block)
2082 // points the compiler error at the `from_rows_par` call site, not at the
2083 // expanded macro internals — cleaner diagnostic for downstream users.
2084 let par_inner_clone_bounds: Vec<TokenStream> = struct_cols
2085 .iter()
2086 .map(|sc| {
2087 let inner_ty = &sc.inner_ty;
2088 quote! { #inner_ty: ::core::clone::Clone, }
2089 })
2090 .collect();
2091 let par_where_clause = if par_inner_clone_bounds.is_empty() {
2092 TokenStream::new()
2093 } else {
2094 quote! { where #(#par_inner_clone_bounds)* }
2095 };
2096
2097 quote! {
2098 /// Parallel row→column transposition using rayon scatter-write.
2099 ///
2100 /// Scalar/expand columns are scatter-written in parallel via rayon.
2101 /// Struct-flattened and `as_list`-on-struct fields are collected
2102 /// sequentially in a pre-pass before the parallel loop (these field
2103 /// types don't implement `Default`, so scatter-write is not possible).
2104 /// Inner struct types must implement `Clone` (enforced by the where
2105 /// clause; the error will point at the `from_rows_par` call site).
2106 ///
2107 /// Always uses rayon — no threshold check. Use `from_rows` for the
2108 /// sequential path.
2109 #[cfg(feature = "rayon")]
2110 #[allow(clippy::uninit_vec)]
2111 pub fn from_rows_par(rows: Vec<#row_name #ty_generics>) -> Self
2112 #par_where_clause
2113 {
2114 use ::miniextendr_api::rayon_bridge::rayon::prelude::*;
2115 let len = rows.len();
2116 #(#par_col_decls)*
2117 #par_loop
2118 #df_name { #par_tag_field #par_len_field #(#par_struct_fields),* }
2119 }
2120 }
2121 } else {
2122 TokenStream::new()
2123 };
2124
2125 // ── IntoIterator (only for named non-empty structs without expansion) ─
2126 let can_iterate = !flat_cols.is_empty()
2127 && !is_tuple_struct
2128 && !is_unit_struct
2129 && !has_expansion
2130 && !has_into_list_struct;
2131 let into_iterator_impl = if can_iterate {
2132 let iterator_name = format_ident!("{}Iterator", df_name);
2133
2134 let iter_field_decls: Vec<_> = flat_cols
2135 .iter()
2136 .map(|fc| {
2137 let name = &fc.df_field;
2138 let ty = &fc.vec_elem_ty;
2139 quote! { #name: std::vec::IntoIter<#ty> }
2140 })
2141 .collect();
2142
2143 let destruct_pattern: Vec<_> = flat_cols
2144 .iter()
2145 .map(|fc| {
2146 let name = &fc.df_field;
2147 quote! { #name }
2148 })
2149 .collect();
2150
2151 let mut iter_init_tokens = TokenStream::new();
2152 for (i, fc) in flat_cols.iter().enumerate() {
2153 let name = &fc.df_field;
2154 let ty = &fc.vec_elem_ty;
2155 if i > 0 {
2156 iter_init_tokens.extend(quote! { , });
2157 }
2158 iter_init_tokens.extend(quote! { #name: <Vec<#ty>>::into_iter(#name) });
2159 }
2160
2161 // For next(): reconstruct original field names (col_name == rust_name for Single)
2162 let mut next_struct_tokens = TokenStream::new();
2163 for (i, rf) in resolved.iter().enumerate() {
2164 if let ResolvedField::Single(data) = rf {
2165 if i > 0 {
2166 next_struct_tokens.extend(quote! { , });
2167 }
2168 let rust_name = &data.rust_name;
2169 let col_name = &data.col_name;
2170 next_struct_tokens.extend(quote! { #rust_name: self.#col_name.next()? });
2171 }
2172 }
2173
2174 let ignore_tag = if has_tag {
2175 quote! { _tag: _, }
2176 } else {
2177 TokenStream::new()
2178 };
2179
2180 // Skipped fields are reconstructed via `Default::default()` each time
2181 // `next()` yields a row. This is why any field type annotated with
2182 // `#[dataframe(skip)]` must implement `Default`.
2183 let skip_defaults: Vec<TokenStream> = skipped_fields
2184 .iter()
2185 .map(|name| quote! { , #name: Default::default() })
2186 .collect();
2187
2188 quote! {
2189 pub struct #iterator_name #impl_generics #where_clause {
2190 #(#iter_field_decls),*
2191 }
2192
2193 impl #impl_generics IntoIterator for #df_name #ty_generics #where_clause {
2194 type Item = #row_name #ty_generics;
2195 type IntoIter = #iterator_name #ty_generics;
2196
2197 fn into_iter(self) -> Self::IntoIter {
2198 let #df_name { #ignore_tag #(#destruct_pattern),* } = self;
2199 #iterator_name {
2200 #iter_init_tokens
2201 }
2202 }
2203 }
2204
2205 impl #impl_generics Iterator for #iterator_name #ty_generics #where_clause {
2206 type Item = #row_name #ty_generics;
2207
2208 fn next(&mut self) -> Option<Self::Item> {
2209 Some(#row_name {
2210 #next_struct_tokens
2211 #(#skip_defaults)*
2212 })
2213 }
2214 }
2215 }
2216 } else {
2217 TokenStream::new()
2218 };
2219 // endregion
2220
2221 // region: Associated methods
2222 let from_dataframe_method = if can_iterate {
2223 quote! {
2224 /// Convert a DataFrame back into a vector of rows.
2225 ///
2226 /// This transposes column-oriented data back into row-oriented format.
2227 pub fn from_dataframe(df: #df_name #ty_generics) -> Vec<Self> {
2228 df.into_iter().collect()
2229 }
2230 }
2231 } else {
2232 TokenStream::new()
2233 };
2234 // endregion
2235
2236 // region: from-R readers (try_from_dataframe / try_from_dataframe_par, #738/#782)
2237 //
2238 // Read an R `data.frame` SEXP directly into `Vec<Self>` without first
2239 // materialising a companion `#df_name`. A reader is generated for every
2240 // *named* struct whose fields are all reader-capable (see
2241 // `field_reader_capable`): scalar `Single` fields, column-expansion fields
2242 // (`[T; N]` / `Vec<T>` + `width`/`expand`), and struct-flatten fields (nested
2243 // `DataFrameRow`). Each shape's reader is the exact inverse of its write rule
2244 // — regroup the suffixed expansion columns, un-prefix and recurse into the
2245 // nested reader. Struct-path `HashMap<String, V>`/`BTreeMap<String, V>` map
2246 // columns read whole via `Vec<map>: TryFromSexp` (#764) — they share the
2247 // scalar `pull_col` path, gated by `SingleFieldData::map_reader` (non-String
2248 // keys / non-scalar values / custom hashers stay reader-incapable). `skip` /
2249 // `as_list` / set columns / tuple / unit shapes are not reader-capable and
2250 // fall through to the trait default (a clear runtime `DataFrameError`).
2251 // Tagged-enum and enum-`Map` readers already landed (#807/#816) — see
2252 // `enum_expansion::build_enum_reader`.
2253 //
2254 // The parallel variant (`#[cfg(feature = "rayon")]`) splits cleanly along the
2255 // R-thread boundary: all SEXP access (column extraction, ALTREP
2256 // materialisation, sub-frame selection + recursive nested reads) happens up
2257 // front on the R/worker thread; only then does `(0..nrow).into_par_iter()`
2258 // assemble each `Self` from the pre-extracted, owned column data by index —
2259 // pure Rust, zero R API calls. Shapes containing a struct-flatten field would
2260 // need `Inner: Clone` for by-index parallel assembly, so their `_par` reader
2261 // delegates to the sequential one (which moves) rather than imposing `Clone`.
2262 let struct_reader = !is_tuple_struct
2263 && !is_unit_struct
2264 && !has_tag
2265 && skipped_fields.is_empty()
2266 && !resolved.is_empty()
2267 && resolved.iter().all(field_reader_capable);
2268
2269 let has_autoexpand_field = resolved
2270 .iter()
2271 .any(|rf| matches!(rf, ResolvedField::AutoExpandVec(_)));
2272 let has_struct_field = resolved
2273 .iter()
2274 .any(|rf| matches!(rf, ResolvedField::Struct(_)));
2275
2276 let reader_methods = if struct_reader {
2277 // Per-field fragments:
2278 // extracts — prelude statements (R-thread): pull/convert columns, run
2279 // length checks, materialise nested sub-frames.
2280 // seq_decls — draining-iterator decls for the sequential row loop.
2281 // seq_builds — `field: expr` in the sequential `Self { … }` literal
2282 // (drains moved columns; indexes only `AutoExpandVec`).
2283 // par_builds — `field: expr` in the parallel `Self { … }` literal
2284 // (by-index `.clone()`; scalars only — never reached when
2285 // a struct-flatten field is present).
2286 let mut extracts: Vec<TokenStream> = Vec::new();
2287 let mut seq_decls: Vec<TokenStream> = Vec::new();
2288 let mut seq_builds: Vec<TokenStream> = Vec::new();
2289 let mut par_builds: Vec<TokenStream> = Vec::new();
2290
2291 // Pull a named column out of R as an owned `Vec<#elem>` via `TryFromSexp`
2292 // (NA-aware, ALTREP-materialising), then length-check it against `__nrow`.
2293 // Bypasses `DataFrame::column` because its `Error = SexpError` bound is
2294 // tighter than the scalar element types' `TryFromSexp::Error`.
2295 let pull_col = |col_var: &syn::Ident, col_name_str: &str, elem_ty: &syn::Type| {
2296 quote! {
2297 let #col_var: Vec<#elem_ty> = {
2298 let __col_sexp = __view.column_raw(#col_name_str).ok_or_else(|| {
2299 ::std::format!("column `{}` is missing from the data.frame", #col_name_str)
2300 })?;
2301 <Vec<#elem_ty> as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__col_sexp)
2302 .map_err(|e| ::std::format!(
2303 "column `{}` could not be converted to the expected type: {}",
2304 #col_name_str, e
2305 ))?
2306 };
2307 if #col_var.len() != __nrow {
2308 return ::core::result::Result::Err(::std::format!(
2309 "column `{}` has length {} but data.frame has {} rows",
2310 #col_name_str, #col_var.len(), __nrow
2311 ));
2312 }
2313 }
2314 };
2315
2316 for rf in &resolved {
2317 match rf {
2318 ResolvedField::Single(data) => {
2319 let rust_name = &data.rust_name;
2320 let col_var = format_ident!("__col_{}", rust_name);
2321 let it_var = format_ident!("__it_{}", rust_name);
2322 match &data.list_elem_ty {
2323 // Un-annotated owned collection: opaque list-column (VECSXP). Read
2324 // each row's element back via `Vec<elem>: TryFromSexp`, then
2325 // `.into()` to the field container type (`Vec<elem>` identity /
2326 // `Box<[elem]>`). A non-list column (e.g. an all-empty column
2327 // materialised as logical-NA) reads back as `__nrow` empty
2328 // collections. (#809)
2329 ::core::option::Option::Some(elem_ty) => {
2330 let field_ty = &data.ty;
2331 let col_name_str = &data.col_name_str;
2332 extracts.push(quote! {
2333 let #col_var: Vec<#field_ty> = {
2334 let __col_sexp = __view.column_raw(#col_name_str).ok_or_else(|| {
2335 ::std::format!("column `{}` is missing from the data.frame", #col_name_str)
2336 })?;
2337 // VECSXP check via `SexpExt::is_list` (UFCS — avoids the
2338 // `List::is_list()` bug that calls `is_pair_list()` instead).
2339 if <::miniextendr_api::SEXP as ::miniextendr_api::SexpExt>::is_list(&__col_sexp) {
2340 let __list = unsafe {
2341 ::miniextendr_api::list::List::from_raw(__col_sexp)
2342 };
2343 let __len = __list.len();
2344 let mut __v: Vec<#field_ty> = ::std::vec::Vec::with_capacity(__len as usize);
2345 for __j in 0..__len {
2346 // in-bounds by construction (0..len)
2347 let __elt = __list.get(__j).unwrap();
2348 let __inner: Vec<#elem_ty> =
2349 <Vec<#elem_ty> as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__elt)
2350 .map_err(|e| ::std::format!(
2351 "column `{}` element {} could not be converted to the expected type: {}",
2352 #col_name_str, __j, e
2353 ))?;
2354 __v.push(::core::convert::Into::into(__inner));
2355 }
2356 __v
2357 } else {
2358 // Non-list column → every row is an empty collection.
2359 (0..__nrow)
2360 .map(|_| ::core::convert::Into::into(::std::vec::Vec::<#elem_ty>::new()))
2361 .collect()
2362 }
2363 };
2364 if #col_var.len() != __nrow {
2365 return ::core::result::Result::Err(::std::format!(
2366 "column `{}` has length {} but data.frame has {} rows",
2367 #col_name_str, #col_var.len(), __nrow
2368 ));
2369 }
2370 });
2371 seq_decls.push(quote! { let mut #it_var = #col_var.into_iter(); });
2372 seq_builds.push(quote! { #rust_name: #it_var.next().unwrap() });
2373 par_builds.push(quote! { #rust_name: #col_var[__i].clone() });
2374 }
2375 // Scalar Single — and reader-capable map Single (#764):
2376 // `pull_col`'s `Vec<#ty>: TryFromSexp` covers both
2377 // (maps via the list-of-named-lists impl).
2378 ::core::option::Option::None => {
2379 extracts.push(pull_col(&col_var, &data.col_name_str, &data.ty));
2380 seq_decls.push(quote! { let mut #it_var = #col_var.into_iter(); });
2381 seq_builds.push(quote! { #rust_name: #it_var.next().unwrap() });
2382 par_builds.push(quote! { #rust_name: #col_var[__i].clone() });
2383 }
2384 }
2385 }
2386 // `[T; N]` → columns `base_1..base_N`, each a plain `Vec<elem>`.
2387 // Regroup into the fixed array per row.
2388 ResolvedField::ExpandedFixed(data) => {
2389 let rust_name = &data.rust_name;
2390 let elem_ty = &data.elem_ty;
2391 let mut it_nexts: Vec<TokenStream> = Vec::new();
2392 let mut idx_clones: Vec<TokenStream> = Vec::new();
2393 for k in 1..=data.len {
2394 let col_var = format_ident!("__ef_{}_{}", rust_name, k);
2395 let it_var = format_ident!("__efit_{}_{}", rust_name, k);
2396 let col_name_str = format!("{}_{}", data.base_name, k);
2397 extracts.push(pull_col(&col_var, &col_name_str, elem_ty));
2398 seq_decls.push(quote! { let mut #it_var = #col_var.into_iter(); });
2399 it_nexts.push(quote! { #it_var.next().unwrap() });
2400 idx_clones.push(quote! { #col_var[__i].clone() });
2401 }
2402 seq_builds.push(quote! { #rust_name: [ #(#it_nexts),* ] });
2403 par_builds.push(quote! { #rust_name: [ #(#idx_clones),* ] });
2404 }
2405 // `Vec<T>` + `width = N` → columns `base_1..base_N`, each
2406 // `Vec<Option<elem>>`. Flatten the N optionals per row back into a
2407 // `Vec<elem>` (trailing-NA padding from the write side drops out).
2408 ResolvedField::ExpandedVec(data) => {
2409 let rust_name = &data.rust_name;
2410 let elem_ty = &data.elem_ty;
2411 let opt_ty: syn::Type = syn::parse_quote!(::core::option::Option<#elem_ty>);
2412 let mut it_nexts: Vec<TokenStream> = Vec::new();
2413 let mut idx_clones: Vec<TokenStream> = Vec::new();
2414 for k in 1..=data.width {
2415 let col_var = format_ident!("__ev_{}_{}", rust_name, k);
2416 let it_var = format_ident!("__evit_{}_{}", rust_name, k);
2417 let col_name_str = format!("{}_{}", data.base_name, k);
2418 extracts.push(pull_col(&col_var, &col_name_str, &opt_ty));
2419 seq_decls.push(quote! { let mut #it_var = #col_var.into_iter(); });
2420 it_nexts.push(quote! { #it_var.next().unwrap() });
2421 idx_clones.push(quote! { #col_var[__i].clone() });
2422 }
2423 // `.into()` converts the collected `Vec<elem>` to the field's
2424 // own container type (`Vec<T>` identity or `Box<[T]>`).
2425 seq_builds.push(quote! {
2426 #rust_name: [ #(#it_nexts),* ]
2427 .into_iter().flatten().collect::<Vec<#elem_ty>>().into()
2428 });
2429 par_builds.push(quote! {
2430 #rust_name: [ #(#idx_clones),* ]
2431 .into_iter().flatten().collect::<Vec<#elem_ty>>().into()
2432 });
2433 }
2434 // `Vec<T>`/`Box<[T]>` + `expand` → a runtime-determined number of
2435 // columns `name_1..name_k`, each `Vec<Option<elem>>`. Discover them
2436 // by walking `name_<i>` until the first gap, then flatten per row.
2437 ResolvedField::AutoExpandVec(data) => {
2438 let rust_name = &data.rust_name;
2439 let elem_ty = &data.elem_ty;
2440 let cols_var = format_ident!("__aev_{}", rust_name);
2441 let col_name_str = &data.col_name_str;
2442 extracts.push(quote! {
2443 let #cols_var: Vec<Vec<::core::option::Option<#elem_ty>>> = {
2444 let mut __cols: Vec<Vec<::core::option::Option<#elem_ty>>> =
2445 ::std::vec::Vec::new();
2446 let mut __k: usize = 1;
2447 loop {
2448 let __cn = ::std::format!("{}_{}", #col_name_str, __k);
2449 match __view.column_raw(&__cn) {
2450 ::core::option::Option::Some(__s) => {
2451 let __c: Vec<::core::option::Option<#elem_ty>> =
2452 <Vec<::core::option::Option<#elem_ty>> as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__s)
2453 .map_err(|e| ::std::format!(
2454 "column `{}` could not be converted to the expected type: {}",
2455 __cn, e
2456 ))?;
2457 if __c.len() != __nrow {
2458 return ::core::result::Result::Err(::std::format!(
2459 "column `{}` has length {} but data.frame has {} rows",
2460 __cn, __c.len(), __nrow
2461 ));
2462 }
2463 __cols.push(__c);
2464 __k += 1;
2465 }
2466 ::core::option::Option::None => break,
2467 }
2468 }
2469 __cols
2470 };
2471 });
2472 // Both seq and par index by row (`__i`): the columns are a
2473 // `Vec<Vec<…>>`, so there is nothing to drain field-wise.
2474 let build = quote! {
2475 #rust_name: #cols_var
2476 .iter()
2477 .filter_map(|__c| __c[__i].clone())
2478 .collect::<Vec<#elem_ty>>()
2479 .into()
2480 };
2481 seq_builds.push(build.clone());
2482 par_builds.push(build);
2483 }
2484 // Nested `DataFrameRow` (#485): the inner type's columns were
2485 // written under a `<base>_` prefix. Select those parent columns,
2486 // strip the prefix into a fresh sub-frame, and recurse through the
2487 // inner type's `DataFrameRowConvert` reader. Routing through the
2488 // trait (rather than `Inner::try_from_dataframe`) keeps this
2489 // compiling even when the inner shape has no reader — it degrades
2490 // to a clear runtime error instead.
2491 ResolvedField::Struct(data) => {
2492 let rust_name = &data.rust_name;
2493 let inner_ty = &data.inner_ty;
2494 let vec_var = format_ident!("__sf_{}", rust_name);
2495 let it_var = format_ident!("__sfit_{}", rust_name);
2496 let base = &data.col_name_str;
2497 let prefix_lit = format!("{}_", data.col_name_str);
2498 extracts.push(quote! {
2499 let #vec_var: Vec<#inner_ty> = {
2500 let __prefix: &str = #prefix_lit;
2501 let __names = __view.names();
2502 let __sel: Vec<&str> = __names
2503 .iter()
2504 .filter(|__n| __n.starts_with(__prefix))
2505 .map(|__n| __n.as_str())
2506 .collect();
2507 if __sel.is_empty() {
2508 return ::core::result::Result::Err(::std::format!(
2509 "struct column `{}`: no columns with prefix `{}` found in the data.frame",
2510 #base, __prefix
2511 ));
2512 }
2513 // `select` builds a fresh list (shared column SEXPs, a
2514 // fresh names vector); protect it across the CHARSXP
2515 // allocations in `strip_prefix` and the recursive read.
2516 let __sub_df = __view.select(&__sel);
2517 let __guard = unsafe {
2518 ::miniextendr_api::OwnedProtect::new(__sub_df.as_sexp())
2519 };
2520 let __sub = ::miniextendr_api::dataframe::DataFrame::from_sexp(__guard.get())
2521 .map_err(|e| e.to_string())?
2522 .strip_prefix(__prefix);
2523 let __out = match <#inner_ty as ::miniextendr_api::dataframe::DataFrameRowConvert>::rows_from_dataframe(&__sub) {
2524 ::core::option::Option::Some(::core::result::Result::Ok(__v)) => __v,
2525 ::core::option::Option::Some(::core::result::Result::Err(__e)) => {
2526 return ::core::result::Result::Err(::std::format!(
2527 "struct column `{}`: {}", #base, __e
2528 ));
2529 }
2530 ::core::option::Option::None => {
2531 return ::core::result::Result::Err(::std::format!(
2532 "struct column `{}`: nested type has no data.frame reader", #base
2533 ));
2534 }
2535 };
2536 drop(__guard);
2537 __out
2538 };
2539 if #vec_var.len() != __nrow {
2540 return ::core::result::Result::Err(::std::format!(
2541 "struct column `{}` produced {} rows but data.frame has {} rows",
2542 #base, #vec_var.len(), __nrow
2543 ));
2544 }
2545 });
2546 seq_decls.push(quote! { let mut #it_var = #vec_var.into_iter(); });
2547 seq_builds.push(quote! { #rust_name: #it_var.next().unwrap() });
2548 par_builds.push(quote! { #rust_name: #vec_var[__i].clone() });
2549 }
2550 // Non-String-keyed map (#919): read two parallel list-columns
2551 // `<base>_keys` / `<base>_values` (VECSXP of typed vectors), then
2552 // zip keys[i] with values[i] back into the map type per row.
2553 // NULL VECSXP elements → empty Vec (not None — struct path uses owned cols).
2554 // All SEXP access happens in `extracts` on the R thread; the
2555 // parallel iterator touches only owned Rust `Vec<Vec<K>>` / `Vec<Vec<V>>`.
2556 ResolvedField::Map(data) => {
2557 let rust_name = &data.rust_name;
2558 let key_ty = &data.key_ty;
2559 let val_ty = &data.val_ty;
2560 let map_ty = &data.map_ty;
2561 let base = &data.base_name;
2562 let keys_col_name = format!("{}_keys", base);
2563 let vals_col_name = format!("{}_values", base);
2564 let keys_var = format_ident!("__mapcol_{}_keys", base.replace('-', "_"));
2565 let vals_var = format_ident!("__mapcol_{}_values", base.replace('-', "_"));
2566 let keys_it_var = format_ident!("__mapcol_it_{}_keys", base.replace('-', "_"));
2567 let vals_it_var =
2568 format_ident!("__mapcol_it_{}_values", base.replace('-', "_"));
2569
2570 // Extract helper: walk a VECSXP list-column; NULL/nil element → empty Vec.
2571 let extract_col = |col_var: &syn::Ident,
2572 col_name: &str,
2573 elem_ty: &syn::Type| {
2574 quote! {
2575 let #col_var: Vec<Vec<#elem_ty>> = {
2576 let __col_sexp = __view.column_raw(#col_name).ok_or_else(|| {
2577 ::std::format!("column `{}` is missing from the data.frame", #col_name)
2578 })?;
2579 if <::miniextendr_api::SEXP as ::miniextendr_api::SexpExt>::is_list(&__col_sexp) {
2580 let __list = unsafe {
2581 ::miniextendr_api::list::List::from_raw(__col_sexp)
2582 };
2583 let __len = __list.len();
2584 let mut __v: Vec<Vec<#elem_ty>> =
2585 ::std::vec::Vec::with_capacity(__len as usize);
2586 for __j in 0..__len {
2587 let __elt = __list.get(__j).unwrap();
2588 if __elt == ::miniextendr_api::SEXP::nil() {
2589 __v.push(::std::vec::Vec::new());
2590 } else {
2591 let __inner: Vec<#elem_ty> =
2592 <Vec<#elem_ty> as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__elt)
2593 .map_err(|e| ::std::format!(
2594 "column `{}` element {} could not be converted: {}",
2595 #col_name, __j, e
2596 ))?;
2597 __v.push(__inner);
2598 }
2599 }
2600 __v
2601 } else {
2602 // Non-list column → all rows have empty maps.
2603 (0..__nrow).map(|_| ::std::vec::Vec::new()).collect()
2604 }
2605 };
2606 if #col_var.len() != __nrow {
2607 return ::core::result::Result::Err(::std::format!(
2608 "column `{}` has length {} but data.frame has {} rows",
2609 #col_name, #col_var.len(), __nrow
2610 ));
2611 }
2612 }
2613 };
2614 extracts.push(extract_col(&keys_var, &keys_col_name, key_ty));
2615 extracts.push(extract_col(&vals_var, &vals_col_name, val_ty));
2616
2617 seq_decls.push(quote! { let mut #keys_it_var = #keys_var.into_iter(); });
2618 seq_decls.push(quote! { let mut #vals_it_var = #vals_var.into_iter(); });
2619 seq_builds.push(quote! {
2620 #rust_name: {
2621 let __k = #keys_it_var.next().unwrap();
2622 let __v = #vals_it_var.next().unwrap();
2623 __k.into_iter().zip(__v).collect::<#map_ty>()
2624 }
2625 });
2626 par_builds.push(quote! {
2627 #rust_name: #keys_var[__i].clone()
2628 .into_iter()
2629 .zip(#vals_var[__i].clone())
2630 .collect::<#map_ty>()
2631 });
2632 }
2633 }
2634 }
2635
2636 // Only `AutoExpandVec` builds reference `__i` in the sequential loop; bind
2637 // the counter only then to avoid an `unused_variables` warning.
2638 let seq_counter = if has_autoexpand_field {
2639 quote! { __i }
2640 } else {
2641 quote! { _ }
2642 };
2643
2644 // A struct-flatten field would need `Inner: Clone` for by-index parallel
2645 // assembly. Rather than impose that, the parallel reader delegates to the
2646 // sequential one (which moves) whenever a struct field is present.
2647 let par_body = if has_struct_field {
2648 quote! { Self::try_from_dataframe(sexp) }
2649 } else {
2650 quote! {
2651 use ::miniextendr_api::rayon_bridge::rayon::prelude::*;
2652 ::miniextendr_api::optionals::parallel::ensure_pool();
2653 let __view = ::miniextendr_api::dataframe::DataFrame::from_sexp(sexp)
2654 .map_err(|e| e.to_string())?;
2655 let __nrow = __view.nrow();
2656 #(#extracts)*
2657 let __rows: Vec<Self> = (0..__nrow)
2658 .into_par_iter()
2659 .map(|__i| Self { #(#par_builds),* })
2660 .collect();
2661 ::core::result::Result::Ok(__rows)
2662 }
2663 };
2664
2665 quote! {
2666 /// Read an R `data.frame` directly into a `Vec<Self>` (sequential).
2667 ///
2668 /// Each column is materialised out of R (NA-aware, ALTREP-materialising)
2669 /// and the rows are assembled by transposing column-major into row-major.
2670 /// Column-expansion fields are regrouped and nested-struct fields are
2671 /// read from their `<field>_`-prefixed sub-frame. Returns `Err` with a
2672 /// descriptive message if a column is missing, mis-typed, or ragged.
2673 pub fn try_from_dataframe(
2674 sexp: ::miniextendr_api::SEXP,
2675 ) -> ::core::result::Result<Vec<Self>, ::std::string::String> {
2676 let __view = ::miniextendr_api::dataframe::DataFrame::from_sexp(sexp)
2677 .map_err(|e| e.to_string())?;
2678 let __nrow = __view.nrow();
2679 #(#extracts)*
2680 #(#seq_decls)*
2681 let mut __rows: Vec<Self> = Vec::with_capacity(__nrow);
2682 for #seq_counter in 0..__nrow {
2683 __rows.push(Self { #(#seq_builds),* });
2684 }
2685 ::core::result::Result::Ok(__rows)
2686 }
2687
2688 /// Read an R `data.frame` directly into a `Vec<Self>` (parallel).
2689 ///
2690 /// Mirrors [`Self::try_from_dataframe`] but assembles rows off the R
2691 /// thread via rayon. Safety: all SEXP access (column extraction, ALTREP
2692 /// materialisation, nested sub-frame reads) happens up front on the
2693 /// R/worker thread; the `into_par_iter()` region touches only
2694 /// pre-extracted owned data and makes no R API calls.
2695 #[cfg(feature = "rayon")]
2696 pub fn try_from_dataframe_par(
2697 sexp: ::miniextendr_api::SEXP,
2698 ) -> ::core::result::Result<Vec<Self>, ::std::string::String> {
2699 #par_body
2700 }
2701 }
2702 } else {
2703 TokenStream::new()
2704 };
2705 // endregion
2706
2707 // region: DataFrame type methods (from_rows, from_rows_par)
2708 let df_methods = quote! {
2709 impl #impl_generics #df_name #ty_generics #where_clause {
2710 /// Sequential row→column transposition.
2711 pub fn from_rows(rows: Vec<#row_name #ty_generics>) -> Self {
2712 rows.into()
2713 }
2714
2715 #from_rows_par_method
2716 }
2717 };
2718
2719 let row_methods = quote! {
2720 impl #impl_generics #row_name #ty_generics #where_clause {
2721 /// Name of the generated DataFrame companion type.
2722 pub const DATAFRAME_TYPE_NAME: &'static str = stringify!(#df_name);
2723
2724 /// Convert a vector of rows into the companion DataFrame type.
2725 ///
2726 /// This transposes row-oriented data into column-oriented format.
2727 pub fn to_dataframe(rows: Vec<Self>) -> #df_name #ty_generics {
2728 rows.into()
2729 }
2730
2731 #from_dataframe_method
2732
2733 #reader_methods
2734 }
2735 };
2736
2737 // Compile-time assertion: row type must implement IntoList
2738 // Skip for unit/empty structs, tuple structs, structs with expansion,
2739 // and structs that store `List`-converted struct fields (#485 as_list).
2740 let trait_check = if !flat_cols.is_empty()
2741 && !is_tuple_struct
2742 && !is_unit_struct
2743 && !has_expansion
2744 && !has_into_list_struct
2745 {
2746 quote! {
2747 const _: () = {
2748 fn _assert_into_list #impl_generics () #where_clause {
2749 fn _check<T: ::miniextendr_api::list::IntoList>() {}
2750 _check::<#row_name #ty_generics>();
2751 }
2752 };
2753 }
2754 } else {
2755 TokenStream::new()
2756 };
2757
2758 // Marker trait impl: struct type implements DataFrameRow via IntoDataFrame chain.
2759 let marker_impl = quote! {
2760 impl #impl_generics ::miniextendr_api::markers::DataFrameRow
2761 for #row_name #ty_generics #where_clause {}
2762 };
2763
2764 // DataFramePayloadFields impl: exposes FIELDS (all resolved column names) and TAG
2765 // (the #[dataframe(tag = "...")] value, or "") for compile-time collision detection
2766 // by outer DataFrameRow enums that nest this type as a struct-flattened field.
2767 let payload_fields_impl = {
2768 // Collect all column names: flat_cols + struct_col base names.
2769 let mut field_names: Vec<String> =
2770 flat_cols.iter().map(|fc| fc.col_name_str.clone()).collect();
2771 for sc in &struct_cols {
2772 field_names.push(sc.col_name_str.clone());
2773 }
2774 let tag_str = attrs.tag.as_deref().unwrap_or("");
2775 quote! {
2776 impl #impl_generics ::miniextendr_api::markers::DataFramePayloadFields
2777 for #row_name #ty_generics #where_clause
2778 {
2779 const FIELDS: &'static [&'static str] = &[#(#field_names),*];
2780 const TAG: &'static str = #tag_str;
2781 }
2782 }
2783 };
2784
2785 // Compile-time assertions for struct-flattened fields (#485): each inner
2786 // type must implement `DataFrameRow`, otherwise users get a confusing
2787 // error pointing at the `to_dataframe` call site instead of the field.
2788 // Note: `Clone` is no longer asserted here — it is enforced via a where
2789 // clause on `from_rows_par` itself, giving a clearer error at the call site.
2790 let struct_assertions: Vec<TokenStream> = struct_cols
2791 .iter()
2792 .map(|sc| {
2793 let inner_ty = &sc.inner_ty;
2794 quote! {
2795 const _: () = {
2796 fn _assert_inner_is_dataframe_row<T: ::miniextendr_api::markers::DataFrameRow>() {}
2797 fn _do_assert #impl_generics () #where_clause {
2798 _assert_inner_is_dataframe_row::<#inner_ty>();
2799 }
2800 };
2801 }
2802 })
2803 .collect();
2804
2805 // region: DataFrameRowConvert on Row — orphan-rule bridge for the public verbs
2806 //
2807 // The derive cannot write `impl IntoDataFrame for Vec<Row>` directly: the orphan rule
2808 // forbids it (both `IntoDataFrame` and `Vec` are foreign in the user crate, and `Row` is
2809 // only *covered* inside `Vec<_>`). Instead it implements the local `DataFrameRowConvert`
2810 // marker on the local `Row`; miniextendr_api's blanket
2811 // `impl<T: DataFrameRowConvert> IntoDataFrame/FromDataFrame for Vec<T>` then gives users the
2812 // public verbs `rows.into_dataframe()?` / `Vec::<Row>::from_dataframe(&df)?`. The methods
2813 // delegate to the companion engine (`to_dataframe` → `ColumnSource::into_dataframe`), the
2814 // merged parallel builder (#777 `from_rows_par`) and reader (#765 `try_from_dataframe[_par]`),
2815 // converting the reader's bare `String` error into the unified `DataFrameError`.
2816
2817 // The parallel build uses the scatter-write builder when one was generated for this shape;
2818 // otherwise it falls back to the sequential transposition.
2819 let has_par_builder = !from_rows_par_method.is_empty();
2820 let rows_into_dataframe_par_body = if has_par_builder {
2821 quote! {
2822 ::miniextendr_api::convert::ColumnSource::into_dataframe(
2823 <#df_name #ty_generics>::from_rows_par(rows),
2824 )
2825 }
2826 } else {
2827 quote! { Self::rows_into_dataframe(rows) }
2828 };
2829
2830 // Readers are overridden for every reader-capable struct shape (scalar,
2831 // column-expansion, struct-flatten — see `struct_reader` / `try_from_dataframe`).
2832 // Other shapes use the trait default (`None`), surfaced by the blanket as a
2833 // clear `DataFrameError`.
2834 let reader_overrides = if struct_reader {
2835 quote! {
2836 fn rows_from_dataframe(
2837 df: &::miniextendr_api::dataframe::DataFrame,
2838 ) -> ::core::option::Option<::core::result::Result<
2839 Vec<Self>,
2840 ::miniextendr_api::dataframe::DataFrameError,
2841 >> {
2842 ::core::option::Option::Some(
2843 <#row_name #ty_generics>::try_from_dataframe(df.as_sexp())
2844 .map_err(::miniextendr_api::dataframe::DataFrameError::Conversion),
2845 )
2846 }
2847
2848 #[cfg(feature = "rayon")]
2849 fn rows_from_dataframe_par(
2850 df: &::miniextendr_api::dataframe::DataFrame,
2851 ) -> ::core::option::Option<::core::result::Result<
2852 Vec<Self>,
2853 ::miniextendr_api::dataframe::DataFrameError,
2854 >> {
2855 ::core::option::Option::Some(
2856 <#row_name #ty_generics>::try_from_dataframe_par(df.as_sexp())
2857 .map_err(::miniextendr_api::dataframe::DataFrameError::Conversion),
2858 )
2859 }
2860 }
2861 } else {
2862 TokenStream::new()
2863 };
2864
2865 let datarow_convert_impl = quote! {
2866 impl #impl_generics ::miniextendr_api::dataframe::DataFrameRowConvert
2867 for #row_name #ty_generics #where_clause
2868 {
2869 fn rows_into_dataframe(
2870 rows: Vec<Self>,
2871 ) -> ::core::result::Result<
2872 ::miniextendr_api::dataframe::DataFrame,
2873 ::miniextendr_api::dataframe::DataFrameError,
2874 > {
2875 ::miniextendr_api::convert::ColumnSource::into_dataframe(
2876 <#row_name #ty_generics>::to_dataframe(rows),
2877 )
2878 }
2879
2880 #[cfg(feature = "rayon")]
2881 fn rows_into_dataframe_par(
2882 rows: Vec<Self>,
2883 ) -> ::core::result::Result<
2884 ::miniextendr_api::dataframe::DataFrame,
2885 ::miniextendr_api::dataframe::DataFrameError,
2886 > {
2887 #rows_into_dataframe_par_body
2888 }
2889
2890 #reader_overrides
2891 }
2892 };
2893 // endregion
2894
2895 Ok(quote! {
2896 #dataframe_struct
2897 #into_dataframe_impl
2898 #from_vec_impl
2899 #df_methods
2900 #into_iterator_impl
2901 #row_methods
2902 #trait_check
2903 #marker_impl
2904 #payload_fields_impl
2905 #datarow_convert_impl
2906 #(#struct_assertions)*
2907 })
2908 // endregion
2909}
2910// endregion
2911
2912// region: Enum align path
2913
2914/// A resolved column in the unified schema across all enum variants.
2915///
2916/// Tracks the column name, element type, which variants contribute to this column,
2917/// and whether the type was coerced to `String` due to cross-variant type conflicts
2918/// (when `#[dataframe(conflicts = "string")]` is active).
2919pub(super) struct ResolvedColumn {
2920 /// Column name in the companion struct / data frame.
2921 pub(super) col_name: syn::Ident,
2922 /// Element type (used as `Vec<Option<#ty>>`).
2923 /// When `string_coerced` is true, this is always `String`.
2924 pub(super) ty: syn::Type,
2925 /// Indices of variants that contain this field.
2926 pub(super) present_in: Vec<usize>,
2927 /// Whether this column was coerced to `String` due to type conflicts.
2928 /// When true, values are converted via `ToString::to_string()` at push time.
2929 pub(super) string_coerced: bool,
2930 /// Whether this column should be emitted as an R factor (via `as_factor` attribute).
2931 /// When `true`, `into_data_frame` wraps the `Vec<Option<T>>` in `FactorOptionVec<T>`
2932 /// before calling `IntoR::into_sexp`, using the `UnitEnumFactor` blanket impl.
2933 pub(super) is_factor: bool,
2934}
2935
2936/// Accumulates unique columns for an enum-to-dataframe unified schema.
2937///
2938/// As columns are registered from each variant's fields, the registry detects
2939/// duplicates and validates type consistency. When `coerce_to_string` is enabled,
2940/// type conflicts are resolved by coercing to `String`; otherwise they produce errors.
2941pub(super) struct ColumnRegistry<'a> {
2942 /// The ordered list of resolved columns in the schema.
2943 pub(super) columns: Vec<ResolvedColumn>,
2944 /// Maps column name strings to their index in `columns` for O(1) dedup lookup.
2945 pub(super) col_index: std::collections::HashMap<String, usize>,
2946 /// Whether to coerce type-conflicting columns to `String` instead of erroring.
2947 pub(super) coerce_to_string: bool,
2948 /// Cached `String` type AST node, used as the coercion target type.
2949 pub(super) string_ty: &'a syn::Type,
2950}
2951
2952impl<'a> ColumnRegistry<'a> {
2953 /// Create a new empty column registry.
2954 fn new(coerce_to_string: bool, string_ty: &'a syn::Type) -> Self {
2955 Self {
2956 columns: Vec::new(),
2957 col_index: std::collections::HashMap::new(),
2958 coerce_to_string,
2959 string_ty,
2960 }
2961 }
2962
2963 /// Register a single column in the schema, or merge with an existing column.
2964 ///
2965 /// If a column with the same name already exists, validates that the types match.
2966 /// On type conflict: coerces to `String` (if `coerce_to_string` is true) or
2967 /// returns `Err`. The `variant_idx` is appended to the column's `present_in` list.
2968 fn register(
2969 &mut self,
2970 col_name: &str,
2971 col_ty: &syn::Type,
2972 variant_idx: usize,
2973 variant_name: &syn::Ident,
2974 error_span: Span,
2975 ) -> syn::Result<()> {
2976 if let Some(&idx) = self.col_index.get(col_name) {
2977 let existing = &self.columns[idx];
2978 if !existing.string_coerced && existing.ty != *col_ty {
2979 if self.coerce_to_string {
2980 self.columns[idx].ty = self.string_ty.clone();
2981 self.columns[idx].string_coerced = true;
2982 } else {
2983 return Err(syn::Error::new(
2984 error_span,
2985 format!(
2986 "type conflict for field `{}`: variant `{}` has a different type \
2987 than a previous variant; \
2988 use `#[dataframe(conflicts = \"string\")]` to coerce all conflicting fields to String",
2989 col_name, variant_name
2990 ),
2991 ));
2992 }
2993 }
2994 self.columns[idx].present_in.push(variant_idx);
2995 } else {
2996 let idx = self.columns.len();
2997 self.columns.push(ResolvedColumn {
2998 col_name: format_ident!("{}", col_name),
2999 ty: col_ty.clone(),
3000 present_in: vec![variant_idx],
3001 string_coerced: false,
3002 is_factor: false,
3003 });
3004 self.col_index.insert(col_name.to_string(), idx);
3005 }
3006 Ok(())
3007 }
3008
3009 /// Like `register`, but marks the column as a factor column (`is_factor = true`).
3010 ///
3011 /// Used for fields annotated with `#[dataframe(as_factor)]`. The companion struct
3012 /// field type stays `Vec<Option<T>>`, but `into_data_frame` wraps it in
3013 /// `FactorOptionVec<T>` (using the `UnitEnumFactor` blanket `IntoR` impl).
3014 pub(super) fn register_factor(
3015 &mut self,
3016 col_name: &str,
3017 col_ty: &syn::Type,
3018 variant_idx: usize,
3019 variant_name: &syn::Ident,
3020 error_span: Span,
3021 ) -> syn::Result<()> {
3022 self.register(col_name, col_ty, variant_idx, variant_name, error_span)?;
3023 if let Some(&idx) = self.col_index.get(col_name) {
3024 self.columns[idx].is_factor = true;
3025 }
3026 Ok(())
3027 }
3028}
3029
3030/// Describes the shape of an enum variant's fields.
3031#[derive(Clone, Copy, PartialEq, Eq)]
3032pub(super) enum VariantShape {
3033 /// `Variant { field: Type, ... }`
3034 Named,
3035 /// `Variant(Type, ...)`
3036 Tuple,
3037 /// `Variant` (no fields)
3038 Unit,
3039}
3040
3041/// A resolved enum field ready for codegen -- either a single column or expanded
3042/// from an array/Vec into multiple suffixed columns.
3043///
3044/// This is the enum-path counterpart of [`ResolvedField`] (used for structs).
3045/// Each variant carries both the binding name (for destructure patterns) and the
3046/// original Rust field name (for error reporting and named-variant patterns).
3047pub(super) enum EnumResolvedField {
3048 /// Single column contribution.
3049 Single(Box<EnumSingleFieldData>),
3050 /// Expanded from [T; N].
3051 ExpandedFixed(Box<EnumExpandedFixedData>),
3052 /// Expanded from `Vec<T>` with pinned width.
3053 ExpandedVec(Box<EnumExpandedVecData>),
3054 /// Auto-expanded `Vec<T>`/`Box<[T]>`: column count determined at runtime.
3055 AutoExpandVec(Box<EnumAutoExpandVecData>),
3056 /// `HashMap<K,V>` or `BTreeMap<K,V>` → two parallel list-columns: `<field>_keys`, `<field>_values`.
3057 Map(Box<EnumMapFieldData>),
3058 /// Struct field whose inner type implements `DataFrameRow` → flattened `<base>_<inner_col>` columns.
3059 Struct(Box<EnumStructFieldData>),
3060}
3061
3062impl EnumResolvedField {
3063 /// Binding name used in destructure patterns.
3064 pub(super) fn binding(&self) -> &syn::Ident {
3065 match self {
3066 Self::Single(data) => &data.binding,
3067 Self::ExpandedFixed(data) => &data.binding,
3068 Self::ExpandedVec(data) => &data.binding,
3069 Self::AutoExpandVec(data) => &data.binding,
3070 Self::Map(data) => &data.binding,
3071 Self::Struct(data) => &data.binding,
3072 }
3073 }
3074
3075 /// Original Rust field name.
3076 pub(super) fn rust_name(&self) -> &syn::Ident {
3077 match self {
3078 Self::Single(data) => &data.rust_name,
3079 Self::ExpandedFixed(data) => &data.rust_name,
3080 Self::ExpandedVec(data) => &data.rust_name,
3081 Self::AutoExpandVec(data) => &data.rust_name,
3082 Self::Map(data) => &data.rust_name,
3083 Self::Struct(data) => &data.rust_name,
3084 }
3085 }
3086}
3087
3088/// Data for [`EnumResolvedField::Single`].
3089pub(super) struct EnumSingleFieldData {
3090 /// Column name in the schema.
3091 pub(super) col_name: syn::Ident,
3092 /// Binding name used in destructure pattern.
3093 pub(super) binding: syn::Ident,
3094 /// Original Rust field name (for named variants).
3095 pub(super) rust_name: syn::Ident,
3096 /// Column type stored in the companion Vec.
3097 ///
3098 /// For most fields this is the raw Rust type. When `needs_into_list` is
3099 /// `true` (struct-typed fields with `#[dataframe(as_list)]`), this is
3100 /// `::miniextendr_api::list::List` — the actual inner type is erased at
3101 /// the storage level and each row value is converted via `.into_list()`.
3102 pub(super) ty: syn::Type,
3103 /// Whether the field's value must be converted via `.into_list()` before
3104 /// being pushed into the companion `Vec<Option<List>>`.
3105 ///
3106 /// Set to `true` only for struct-typed fields (`FieldTypeKind::Struct`)
3107 /// that carry `#[dataframe(as_list)]`. The companion struct field type is
3108 /// `Vec<Option<::miniextendr_api::list::List>>` in this case.
3109 pub(super) needs_into_list: bool,
3110 /// Whether the field should be emitted as an R factor column.
3111 ///
3112 /// Set to `true` for fields annotated with `#[dataframe(as_factor)]`.
3113 /// The companion struct field type is `Vec<Option<T>>` (unchanged), but
3114 /// `into_data_frame` wraps it in `FactorOptionVec<T>` to use the
3115 /// `UnitEnumFactor`-based blanket `IntoR` impl.
3116 pub(super) is_factor: bool,
3117}
3118
3119/// Data for [`EnumResolvedField::ExpandedFixed`].
3120pub(super) struct EnumExpandedFixedData {
3121 /// Base column name.
3122 pub(super) base_name: String,
3123 /// Binding name.
3124 pub(super) binding: syn::Ident,
3125 /// Original Rust field name.
3126 pub(super) rust_name: syn::Ident,
3127 /// Element type.
3128 pub(super) elem_ty: syn::Type,
3129 /// Array length.
3130 pub(super) len: usize,
3131}
3132
3133/// Data for [`EnumResolvedField::ExpandedVec`].
3134pub(super) struct EnumExpandedVecData {
3135 /// Base column name.
3136 pub(super) base_name: String,
3137 /// Binding name.
3138 pub(super) binding: syn::Ident,
3139 /// Original Rust field name.
3140 pub(super) rust_name: syn::Ident,
3141 /// Element type.
3142 pub(super) elem_ty: syn::Type,
3143 /// Pinned width.
3144 pub(super) width: usize,
3145}
3146
3147/// Data for [`EnumResolvedField::AutoExpandVec`].
3148pub(super) struct EnumAutoExpandVecData {
3149 /// Base column name.
3150 pub(super) base_name: String,
3151 /// Binding name.
3152 pub(super) binding: syn::Ident,
3153 /// Original Rust field name.
3154 pub(super) rust_name: syn::Ident,
3155 /// Element type.
3156 pub(super) elem_ty: syn::Type,
3157 /// Container type for companion struct (`Vec<T>` or `Box<[T]>`).
3158 pub(super) container_ty: syn::Type,
3159}
3160
3161/// Data for [`EnumResolvedField::Map`].
3162///
3163/// A `HashMap<K,V>` or `BTreeMap<K,V>` field expands to two parallel list-columns:
3164/// `<base_name>_keys: Vec<Option<Vec<K>>>` and `<base_name>_values: Vec<Option<Vec<V>>>`.
3165/// Absent-variant rows get `None` in both columns. Key order follows the map's own
3166/// iteration order: `BTreeMap` yields sorted keys, `HashMap` yields non-deterministic order.
3167/// Both are produced via `into_iter().unzip()` which guarantees pairwise alignment.
3168pub(super) struct EnumMapFieldData {
3169 /// Base column name (field name or `rename` override).
3170 pub(super) base_name: String,
3171 /// Binding name used in destructure pattern.
3172 pub(super) binding: syn::Ident,
3173 /// Original Rust field name.
3174 pub(super) rust_name: syn::Ident,
3175 /// Key type K.
3176 pub(super) key_ty: syn::Type,
3177 /// Value type V.
3178 pub(super) val_ty: syn::Type,
3179 /// Full original field type (`HashMap<K, V>` / `BTreeMap<K, V>`). The reader
3180 /// regroups the `_keys`/`_values` list-columns and `collect()`s back into this
3181 /// exact map type — both `HashMap` and `BTreeMap` implement `FromIterator<(K, V)>`.
3182 pub(super) map_ty: syn::Type,
3183}
3184
3185/// Data for [`EnumResolvedField::Struct`].
3186///
3187/// A field whose inner type implements `DataFrameRow` expands to `<base_name>_<inner_col>`
3188/// prefixed columns — one output column per column emitted by the inner type's companion
3189/// DataFrame. Absent-variant rows produce `None` in every prefixed column.
3190///
3191/// The companion struct holds `Vec<Option<Inner>>` (not `Vec<Inner>`). The `into_data_frame`
3192/// method collects present rows into a dense `Vec<Inner>` (tracking presence indices),
3193/// calls `Inner::to_dataframe(present_rows)`, extracts named column SEXPs, and scatters
3194/// them back to the full row count with `None`-fill for absent rows.
3195pub(super) struct EnumStructFieldData {
3196 /// Base name for column prefixing (field name or `rename` override).
3197 pub(super) base_name: String,
3198 /// Binding name used in destructure pattern.
3199 pub(super) binding: syn::Ident,
3200 /// Original Rust field name.
3201 pub(super) rust_name: syn::Ident,
3202 /// Inner struct type (used for the compile-time DataFrameRow assertion and codegen).
3203 pub(super) inner_ty: syn::Type,
3204}
3205
3206/// Parsed and resolved information about a single enum variant for DataFrame codegen.
3207///
3208/// Contains the variant's name, shape (named/tuple/unit), resolved fields (after
3209/// applying `#[dataframe(...)]` attributes and type classification), and any
3210/// skipped field names (needed for complete destructure patterns in named variants).
3211pub(super) struct VariantInfo {
3212 /// Variant name.
3213 pub(super) name: syn::Ident,
3214 /// Shape of this variant.
3215 pub(super) shape: VariantShape,
3216 /// Resolved fields (after applying field attrs + type classification).
3217 pub(super) fields: Vec<EnumResolvedField>,
3218 /// Original Rust field names (for named variants) — needed for skipped fields in destructure.
3219 pub(super) skipped_fields: Vec<syn::Ident>,
3220}
3221// endregion
3222
3223// region: Enum-specific expansion (in sub-module)
3224
3225mod enum_expansion;
3226use enum_expansion::derive_enum_dataframe;
3227// endregion
3228
3229// region: tests
3230#[cfg(test)]
3231mod tests {
3232 use super::*;
3233
3234 /// Stringify the derive output (whitespace-normalised) for substring assertions.
3235 fn expand(input: DeriveInput) -> String {
3236 derive_dataframe_row(input).unwrap().to_string()
3237 }
3238
3239 /// Scalar named struct: the baseline `try_from_dataframe_par` shape (#765).
3240 /// Both the sequential and parallel readers must be emitted, and the parallel
3241 /// one must drive a `into_par_iter()` row-assembly region.
3242 #[test]
3243 fn scalar_struct_gets_parallel_reader() {
3244 let code = expand(syn::parse_quote! {
3245 #[derive(DataFrameRow)]
3246 struct Measurement {
3247 time: f64,
3248 value: f64,
3249 }
3250 });
3251 assert!(code.contains("fn try_from_dataframe"));
3252 assert!(code.contains("fn try_from_dataframe_par"));
3253 assert!(code.contains("into_par_iter"));
3254 }
3255
3256 /// `[T; N]` fixed-array expansion field (#782/#808): the reader regroups the
3257 /// `pos_1`/`pos_2` columns back into the array inside the parallel loop, with
3258 /// zero SEXP access in `into_par_iter` (the invariant #764 protects).
3259 #[test]
3260 fn fixed_array_struct_gets_parallel_reader() {
3261 let code = expand(syn::parse_quote! {
3262 #[derive(DataFrameRow)]
3263 struct Point {
3264 #[dataframe(rename = "pos")]
3265 pos: [f64; 2],
3266 }
3267 });
3268 assert!(code.contains("fn try_from_dataframe_par"));
3269 assert!(code.contains("into_par_iter"));
3270 // Regrouped from the suffixed expansion columns.
3271 assert!(code.contains("pos_1"));
3272 assert!(code.contains("pos_2"));
3273 }
3274
3275 /// `Vec<T>` + `width = N` expansion field (#782/#808): the reader flattens the
3276 /// `scores_1`/`scores_2` Option columns per row back into the vec.
3277 #[test]
3278 fn pinned_vec_struct_gets_parallel_reader() {
3279 let code = expand(syn::parse_quote! {
3280 #[derive(DataFrameRow)]
3281 struct Scored {
3282 #[dataframe(width = 2)]
3283 scores: Vec<i32>,
3284 }
3285 });
3286 assert!(code.contains("fn try_from_dataframe_par"));
3287 assert!(code.contains("into_par_iter"));
3288 assert!(code.contains("scores_1"));
3289 assert!(code.contains("scores_2"));
3290 }
3291
3292 /// `Vec<T>` + `expand` auto-expansion field (#782/#808): the reader discovers
3293 /// `tags_<i>` columns at runtime and flattens per row. Still a true parallel
3294 /// reader (the column discovery happens on the R thread, up front).
3295 #[test]
3296 fn auto_expand_struct_gets_parallel_reader() {
3297 let code = expand(syn::parse_quote! {
3298 #[derive(DataFrameRow)]
3299 struct Tagged {
3300 #[dataframe(expand)]
3301 tags: Vec<i32>,
3302 }
3303 });
3304 assert!(code.contains("fn try_from_dataframe_par"));
3305 assert!(code.contains("into_par_iter"));
3306 }
3307
3308 /// Struct-flatten field (#485/#808): the struct still gets readers, but the
3309 /// parallel variant deliberately delegates to the sequential one to avoid
3310 /// imposing `Inner: Clone` for by-index parallel assembly (#764 design note).
3311 #[test]
3312 fn struct_flatten_par_delegates_to_sequential() {
3313 let code = expand(syn::parse_quote! {
3314 #[derive(DataFrameRow)]
3315 struct Outer {
3316 id: i32,
3317 inner: Inner,
3318 }
3319 });
3320 assert!(code.contains("fn try_from_dataframe"));
3321 assert!(code.contains("fn try_from_dataframe_par"));
3322 // The `_par` body delegates rather than running its own `into_par_iter`.
3323 assert!(code.contains("Self :: try_from_dataframe (sexp)"));
3324 }
3325
3326 /// Tagged enum companion (#807/#816): enums now get full readers too, including
3327 /// a parallel per-row tag-dispatch loop. Documents that the #764 "no reader at
3328 /// all today" framing for enums is now stale.
3329 #[test]
3330 fn tagged_enum_gets_parallel_reader() {
3331 let code = expand(syn::parse_quote! {
3332 #[derive(DataFrameRow)]
3333 #[dataframe(tag = "_type")]
3334 enum Event {
3335 Click { x: i32, y: i32 },
3336 Key { code: i32 },
3337 }
3338 });
3339 assert!(code.contains("fn try_from_dataframe"));
3340 assert!(code.contains("fn try_from_dataframe_par"));
3341 assert!(code.contains("into_par_iter"));
3342 }
3343
3344 /// Struct-path `HashMap<String, V>` map column (#764): the list-of-named-lists
3345 /// column reads back whole via `Vec<map>: TryFromSexp` on the R thread, so the
3346 /// struct gets both readers (it shares the scalar `pull_col` path — zero SEXP
3347 /// access inside `into_par_iter`). Flips the pre-#764 lock-in test from #920.
3348 #[test]
3349 fn struct_with_string_keyed_map_field_gets_parallel_reader() {
3350 let code = expand(syn::parse_quote! {
3351 #[derive(DataFrameRow)]
3352 struct Config {
3353 opts: ::std::collections::HashMap<String, i32>,
3354 }
3355 });
3356 assert!(code.contains("fn try_from_dataframe"));
3357 assert!(code.contains("fn try_from_dataframe_par"));
3358 assert!(code.contains("into_par_iter"));
3359 }
3360
3361 /// `BTreeMap<String, Option<scalar>>` is also reader-capable: the
3362 /// `Vec<map>: TryFromSexp` impl is generic over `V: TryFromSexp`, and
3363 /// `Option<scalar>` qualifies (NULL list elements → `None`).
3364 #[test]
3365 fn struct_with_btreemap_option_value_gets_reader() {
3366 let code = expand(syn::parse_quote! {
3367 #[derive(DataFrameRow)]
3368 struct Config {
3369 opts: ::std::collections::BTreeMap<String, Option<f64>>,
3370 }
3371 });
3372 assert!(code.contains("fn try_from_dataframe"));
3373 assert!(code.contains("fn try_from_dataframe_par"));
3374 }
3375
3376 /// Non-`String` bare-scalar map keys (#919): the struct gets parallel `_keys`/`_values`
3377 /// list-columns and both readers. The write side uses `Vec<Vec<K>>/Vec<Vec<V>>: IntoR`
3378 /// (VECSXP of typed vectors); the read side zips them back into the map type per row.
3379 #[test]
3380 fn struct_with_non_string_keyed_map_gets_parallel_reader() {
3381 let code = expand(syn::parse_quote! {
3382 #[derive(DataFrameRow)]
3383 struct Config {
3384 opts: ::std::collections::HashMap<i32, f64>,
3385 }
3386 });
3387 assert!(
3388 code.contains("fn try_from_dataframe"),
3389 "non-String bare-scalar map keys should produce a reader via _keys/_values columns"
3390 );
3391 assert!(
3392 code.contains("fn try_from_dataframe_par"),
3393 "non-String bare-scalar map keys should produce a parallel reader"
3394 );
3395 assert!(
3396 code.contains("opts_keys"),
3397 "non-String map field `opts` should expand to `opts_keys` column"
3398 );
3399 assert!(
3400 code.contains("opts_values"),
3401 "non-String map field `opts` should expand to `opts_values` column"
3402 );
3403 }
3404
3405 /// Same as above but using an unqualified path `std::collections::BTreeMap` (no
3406 /// leading `::`) — path form used in actual rpkg fixtures.
3407 #[test]
3408 fn struct_with_btreemap_int_key_unqualified_path() {
3409 let code = expand(syn::parse_quote! {
3410 #[derive(DataFrameRow)]
3411 struct Tally {
3412 id: i32,
3413 tally: std::collections::BTreeMap<i32, f64>,
3414 }
3415 });
3416 assert!(
3417 code.contains("tally_keys"),
3418 "unqualified std::collections::BTreeMap<i32, f64> must expand to tally_keys"
3419 );
3420 assert!(
3421 code.contains("fn try_from_dataframe"),
3422 "unqualified BTreeMap<i32, f64> should produce a reader"
3423 );
3424 }
3425
3426 /// Float-keyed maps (`f32`/`f64`) are rejected with a clear error.
3427 #[test]
3428 #[should_panic]
3429 fn struct_with_float_keyed_map_is_rejected() {
3430 let _code = expand(syn::parse_quote! {
3431 #[derive(DataFrameRow)]
3432 struct Config {
3433 opts: ::std::collections::HashMap<f64, i32>,
3434 }
3435 });
3436 }
3437
3438 /// Custom-hasher maps (`HashMap<K, V, S>`) are rejected from the reader path:
3439 /// the `Vec<HashMap<String, V>>: TryFromSexp` impl only covers the default
3440 /// hasher, so emitting a reader would not compile.
3441 #[test]
3442 fn struct_with_custom_hasher_map_has_no_reader() {
3443 let code = expand(syn::parse_quote! {
3444 #[derive(DataFrameRow)]
3445 struct Config {
3446 opts: ::std::collections::HashMap<String, i32, MyHasher>,
3447 }
3448 });
3449 assert!(
3450 !code.contains("fn try_from_dataframe"),
3451 "custom-hasher maps lack `Vec<map>: TryFromSexp`; the reader must stay gated out"
3452 );
3453 }
3454}
3455// endregion