miniextendr_macros/dataframe_derive/enum_expansion.rs
1//! Enum-specific DataFrame derive expansion.
2//!
3//! Generates a companion struct where every column is `Vec<Option<T>>`, with
4//! `None` fill for fields absent in a given variant.
5
6use proc_macro2::TokenStream;
7use quote::{ToTokens, format_ident, quote};
8use syn::{DeriveInput, Fields};
9
10use super::{
11 ColumnRegistry, DataFrameAttrs, EnumAutoExpandVecData, EnumExpandedFixedData,
12 EnumExpandedVecData, EnumMapFieldData, EnumResolvedField, EnumSingleFieldData,
13 EnumStructFieldData, FieldTypeKind, ResolvedColumn, VariantInfo, VariantShape,
14 classify_field_type, is_bare_reader_scalar_ty, is_reader_scalar_ty, parse_field_attrs,
15};
16use crate::naming;
17use std::collections::HashMap;
18
19/// Derive `DataFrameRow` for an enum with `#[dataframe(align)]`.
20///
21/// Generates a companion struct where every column is `Vec<Option<T>>`, with
22/// `None` fill for fields absent in a given variant. This is the enum counterpart
23/// of [`super::derive_struct_dataframe`].
24///
25/// # Generated items
26///
27/// - Companion struct `{Name}DataFrame` with `Vec<Option<T>>` columns (field-name union)
28/// - Optional `_tag: Vec<String>` column for variant discrimination
29/// - `impl IntoDataFrame` (companion → R data.frame) and `impl ColumnarFrame`
30/// (rows ↔ the pure-Rust companion: `from_rows` / `from_rows_par`)
31/// - `impl From<Vec<Enum>>` (sequential row->column transposition)
32/// - `impl DataFrameRowSplit` on the enum — the split
33/// (one-`data.frame`-per-variant) representation, surfaced as
34/// `rows.into_dataframe_split()` via `IntoDataFrameSplit`'s blanket impl
35///
36/// The write/read verbs are the trait surface — `rows.into_dataframe()?`
37/// (`IntoDataFrame` / `AsDataFrameExt`), `rows.into_dataframe_split()`
38/// (`IntoDataFrameSplit`), `Vec::<Row>::from_dataframe(&df)?`
39/// (`FromDataFrame`) or the one-call `Row::try_from_dataframe(sexp)` reader, and the
40/// pure-Rust `ColumnarFrame` companion verbs. The row type keeps two `#[doc(hidden)]`
41/// helpers: `to_dataframe(rows) -> companion` for the nested-companion write path
42/// (`Inner::to_dataframe(..)` without naming the inner companion type) and
43/// `to_dataframe_split(rows)`, the body holder behind the `DataFrameRowSplit` bridge.
44///
45/// # Variant support
46///
47/// - Named variants (`{ field: T }`): fields contribute by name to the unified schema
48/// - Tuple variants (`(T, U)`): fields are named `_0`, `_1`, etc.
49/// - Unit variants: contribute no columns (only tag if present)
50///
51/// # Auto-expand fields
52///
53/// Fields with `#[dataframe(expand)]` on `Vec<T>` types get dynamic column counts
54/// determined at runtime from the maximum row length across all rows. These are
55/// tracked separately from the static [`ColumnRegistry`].
56///
57/// Returns `Err` if the enum has no variants or if type conflicts arise without
58/// `#[dataframe(conflicts = "string")]`.
59pub(super) fn derive_enum_dataframe(
60 row_name: &syn::Ident,
61 input: &DeriveInput,
62 data: &syn::DataEnum,
63 df_name: &syn::Ident,
64 attrs: &DataFrameAttrs,
65) -> syn::Result<TokenStream> {
66 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
67
68 // region: Validate variants
69 if data.variants.is_empty() {
70 return Err(syn::Error::new_spanned(
71 row_name,
72 "DataFrameRow requires at least one variant",
73 ));
74 }
75
76 let mut variant_infos: Vec<VariantInfo> = Vec::new();
77
78 for variant in &data.variants {
79 match &variant.fields {
80 Fields::Named(fields) => {
81 let mut resolved = Vec::new();
82 let mut skipped = Vec::new();
83 for f in &fields.named {
84 let fa = parse_field_attrs(f)?;
85 let rust_name = f.ident.as_ref().unwrap().clone();
86 if fa.skip {
87 skipped.push(rust_name);
88 continue;
89 }
90 let col_name_str = fa.rename.unwrap_or_else(|| rust_name.to_string());
91 let binding = format_ident!("__v_{}", rust_name);
92
93 if fa.as_list {
94 // Struct-typed fields with `as_list` must be converted via `into_list()`
95 // at `into_data_frame` time. We keep the original Rust type in the
96 // companion struct (so no R API is called during row accumulation) and
97 // flag `needs_into_list = true` to trigger per-element conversion in the
98 // dynamic `into_data_frame` path.
99 //
100 // Use `.as_ref().ok()` to suppress classification errors: `as_list` is
101 // an explicit opt-in, so wrapper types (Option<T>, Arc<T>, …) are
102 // allowed — they become opaque list-columns.
103 let needs_into_list = matches!(
104 classify_field_type(&f.ty).as_ref().ok(),
105 Some(FieldTypeKind::Struct { .. })
106 );
107 resolved.push(EnumResolvedField::Single(Box::new(EnumSingleFieldData {
108 col_name: format_ident!("{}", col_name_str),
109 binding: binding.clone(),
110 rust_name: rust_name.clone(),
111 ty: f.ty.clone(),
112 needs_into_list,
113 is_factor: false,
114 })));
115 } else if fa.as_factor {
116 // `as_factor` is only valid on bare-ident enum types (Struct kind).
117 // The inner enum must be unit-only and derive DataFrameRow, which
118 // auto-emits UnitEnumFactor so FactorOptionVec<T> implements IntoR.
119 match classify_field_type(&f.ty)? {
120 FieldTypeKind::Struct { .. } => {
121 resolved.push(EnumResolvedField::Single(Box::new(
122 EnumSingleFieldData {
123 col_name: format_ident!("{}", col_name_str),
124 binding: binding.clone(),
125 rust_name: rust_name.clone(),
126 ty: f.ty.clone(),
127 needs_into_list: false,
128 is_factor: true,
129 },
130 )));
131 }
132 _ => {
133 return Err(syn::Error::new_spanned(
134 &f.ty,
135 "`as_factor` is only valid on bare-ident enum/struct types; \
136 use `as_list` for generic or complex types, or remove \
137 `as_factor` for scalar fields",
138 ));
139 }
140 }
141 } else {
142 match classify_field_type(&f.ty)? {
143 FieldTypeKind::FixedArray(elem_ty, len) => {
144 resolved.push(EnumResolvedField::ExpandedFixed(Box::new(
145 EnumExpandedFixedData {
146 base_name: col_name_str,
147 binding: binding.clone(),
148 rust_name: rust_name.clone(),
149 elem_ty: elem_ty.clone(),
150 len,
151 },
152 )));
153 }
154 FieldTypeKind::VariableVec(elem_ty)
155 | FieldTypeKind::BoxedSlice(elem_ty)
156 | FieldTypeKind::BorrowedSlice(elem_ty) => {
157 if let Some(width) = fa.width {
158 resolved.push(EnumResolvedField::ExpandedVec(Box::new(
159 EnumExpandedVecData {
160 base_name: col_name_str,
161 binding: binding.clone(),
162 rust_name: rust_name.clone(),
163 elem_ty: elem_ty.clone(),
164 width,
165 },
166 )));
167 } else if fa.expand {
168 resolved.push(EnumResolvedField::AutoExpandVec(Box::new(
169 EnumAutoExpandVecData {
170 base_name: col_name_str,
171 binding: binding.clone(),
172 rust_name: rust_name.clone(),
173 elem_ty: elem_ty.clone(),
174 container_ty: f.ty.clone(),
175 },
176 )));
177 } else {
178 resolved.push(EnumResolvedField::Single(Box::new(
179 EnumSingleFieldData {
180 col_name: format_ident!("{}", col_name_str),
181 binding: binding.clone(),
182 rust_name: rust_name.clone(),
183 ty: f.ty.clone(),
184 needs_into_list: false,
185 is_factor: false,
186 },
187 )));
188 }
189 }
190 FieldTypeKind::Map { key_ty, val_ty } => {
191 if fa.width.is_some() {
192 return Err(syn::Error::new_spanned(
193 &f.ty,
194 "`width` is not valid on HashMap/BTreeMap fields",
195 ));
196 }
197 if fa.expand {
198 return Err(syn::Error::new_spanned(
199 &f.ty,
200 "`expand`/`unnest` is not valid on HashMap/BTreeMap fields",
201 ));
202 }
203 resolved.push(EnumResolvedField::Map(Box::new(EnumMapFieldData {
204 base_name: col_name_str,
205 binding: binding.clone(),
206 rust_name: rust_name.clone(),
207 key_ty: key_ty.clone(),
208 val_ty: val_ty.clone(),
209 map_ty: f.ty.clone(),
210 })));
211 }
212 FieldTypeKind::Struct { inner_ty } => {
213 if fa.width.is_some() {
214 return Err(syn::Error::new_spanned(
215 &f.ty,
216 "`width` is not valid on struct fields; use \
217 `#[dataframe(as_list)]` to keep as an opaque list-column",
218 ));
219 }
220 if fa.expand {
221 return Err(syn::Error::new_spanned(
222 &f.ty,
223 "`expand`/`unnest` is not valid on struct fields; struct \
224 fields flatten by default via their `DataFrameRow` impl",
225 ));
226 }
227 resolved.push(EnumResolvedField::Struct(Box::new(
228 EnumStructFieldData {
229 base_name: col_name_str,
230 binding: binding.clone(),
231 rust_name: rust_name.clone(),
232 inner_ty: inner_ty.clone(),
233 },
234 )));
235 }
236 FieldTypeKind::Scalar => {
237 resolved.push(EnumResolvedField::Single(Box::new(
238 EnumSingleFieldData {
239 col_name: format_ident!("{}", col_name_str),
240 binding: binding.clone(),
241 rust_name: rust_name.clone(),
242 ty: f.ty.clone(),
243 needs_into_list: false,
244 is_factor: false,
245 },
246 )));
247 }
248 }
249 }
250 }
251 // B1: Check for `<base>_<inner_tag>` discriminant column collision.
252 //
253 // When a Struct field `kind: Inner` is flattened, the inner enum's
254 // discriminant column (tag) is emitted under `<base>_<inner_tag>`.
255 // The inner tag is retrieved at runtime from
256 // `<Inner as DataFramePayloadFields>::TAG`; the B1 check here uses the
257 // hardcoded default `"variant"` for compile-time sibling detection because
258 // we cannot inspect inner enum attributes from the outer macro parse phase.
259 // The per-inner-field payload collision is caught separately via the
260 // `const _:` assertions emitted below (using `DataFramePayloadFields`).
261 //
262 // We detect the following cases at compile time (both using "variant"):
263 // 1. Struct field `kind: Inner` + Single/Scalar sibling named `kind_variant`
264 // 2. Struct field `kind: Inner` + another Struct sibling field renamed to
265 // produce `kind_variant`
266 //
267 // Inner-enum-internal collision (Inner has both `tag = "X"` AND payload
268 // field `X`) is caught by the `assert_no_payload_field_collision` const
269 // assertion emitted below — no carve-out needed.
270 {
271 // Collect every flat column name produced by non-Struct resolved fields.
272 let flat_col_names: Vec<String> = resolved
273 .iter()
274 .filter_map(|r| match r {
275 EnumResolvedField::Single(d) => Some(d.col_name.to_string()),
276 EnumResolvedField::Map(d) => {
277 // Map fields produce <base>_keys and <base>_values.
278 // Neither collides with <struct>_variant unless someone
279 // explicitly renamed to match — covered by the Struct check
280 // via base_name.
281 let _ = d;
282 None
283 }
284 _ => None,
285 })
286 .collect();
287
288 for r in &resolved {
289 if let EnumResolvedField::Struct(struct_data) = r {
290 // Use hardcoded "variant" for the sibling check — this is the
291 // default inner tag. The inner-payload collision for non-default
292 // tags is caught by assert_no_payload_field_collision below.
293 let discriminant_col = format!("{}_variant", struct_data.base_name);
294 if flat_col_names.contains(&discriminant_col) {
295 // Find the colliding field for a better span.
296 let colliding_span = resolved
297 .iter()
298 .find_map(|r2| match r2 {
299 EnumResolvedField::Single(d)
300 if d.col_name == discriminant_col.as_str() =>
301 {
302 Some(d.col_name.span())
303 }
304 _ => None,
305 })
306 .unwrap_or_else(proc_macro2::Span::call_site);
307 return Err(syn::Error::new(
308 colliding_span,
309 format!(
310 "column name collision: the flatten field `{base}` \
311 (a nested `DataFrameRow` enum) will emit a \
312 discriminant column named `{disc}`, but a sibling \
313 field already produces a column with the same name. \
314 Rename the sibling field or use \
315 `#[dataframe(tag = \"...\")]` on the inner enum to \
316 choose a different discriminant column name \
317 (e.g. `#[dataframe(tag = \"type\")]` → `{base}_type`)",
318 base = struct_data.base_name,
319 disc = discriminant_col,
320 ),
321 ));
322 }
323 }
324 }
325 }
326 variant_infos.push(VariantInfo {
327 name: variant.ident.clone(),
328 shape: VariantShape::Named,
329 fields: resolved,
330 skipped_fields: skipped,
331 });
332 }
333 Fields::Unnamed(fields) => {
334 let mut resolved = Vec::new();
335 for (i, f) in fields.unnamed.iter().enumerate() {
336 let fa = parse_field_attrs(f)?;
337 let rust_name = format_ident!("_{}", i);
338 if fa.skip {
339 continue;
340 }
341 let col_name_str = fa.rename.unwrap_or_else(|| rust_name.to_string());
342 let binding = format_ident!("__v_{}", rust_name);
343
344 // Tuple enum fields: same expansion logic
345 if fa.as_list {
346 // Use `.as_ref().ok()` to suppress classification errors: `as_list` is
347 // an explicit opt-in, so wrapper types (Option<T>, Arc<T>, …) are
348 // allowed — they become opaque list-columns.
349 let needs_into_list = matches!(
350 classify_field_type(&f.ty).as_ref().ok(),
351 Some(FieldTypeKind::Struct { .. })
352 );
353 resolved.push(EnumResolvedField::Single(Box::new(EnumSingleFieldData {
354 col_name: format_ident!("{}", col_name_str),
355 binding,
356 rust_name,
357 ty: f.ty.clone(),
358 needs_into_list,
359 is_factor: false,
360 })));
361 } else if fa.as_factor {
362 match classify_field_type(&f.ty)? {
363 FieldTypeKind::Struct { .. } => {
364 resolved.push(EnumResolvedField::Single(Box::new(
365 EnumSingleFieldData {
366 col_name: format_ident!("{}", col_name_str),
367 binding,
368 rust_name,
369 ty: f.ty.clone(),
370 needs_into_list: false,
371 is_factor: true,
372 },
373 )));
374 }
375 _ => {
376 return Err(syn::Error::new_spanned(
377 &f.ty,
378 "`as_factor` is only valid on bare-ident enum/struct types; \
379 use `as_list` for generic or complex types, or remove \
380 `as_factor` for scalar fields",
381 ));
382 }
383 }
384 } else {
385 match classify_field_type(&f.ty)? {
386 FieldTypeKind::FixedArray(elem_ty, len) => {
387 resolved.push(EnumResolvedField::ExpandedFixed(Box::new(
388 EnumExpandedFixedData {
389 base_name: col_name_str,
390 binding,
391 rust_name,
392 elem_ty: elem_ty.clone(),
393 len,
394 },
395 )));
396 }
397 FieldTypeKind::VariableVec(elem_ty)
398 | FieldTypeKind::BoxedSlice(elem_ty)
399 | FieldTypeKind::BorrowedSlice(elem_ty) => {
400 if let Some(width) = fa.width {
401 resolved.push(EnumResolvedField::ExpandedVec(Box::new(
402 EnumExpandedVecData {
403 base_name: col_name_str,
404 binding,
405 rust_name,
406 elem_ty: elem_ty.clone(),
407 width,
408 },
409 )));
410 } else if fa.expand {
411 resolved.push(EnumResolvedField::AutoExpandVec(Box::new(
412 EnumAutoExpandVecData {
413 base_name: col_name_str,
414 binding,
415 rust_name,
416 elem_ty: elem_ty.clone(),
417 container_ty: f.ty.clone(),
418 },
419 )));
420 } else {
421 resolved.push(EnumResolvedField::Single(Box::new(
422 EnumSingleFieldData {
423 col_name: format_ident!("{}", col_name_str),
424 binding,
425 rust_name,
426 ty: f.ty.clone(),
427 needs_into_list: false,
428 is_factor: false,
429 },
430 )));
431 }
432 }
433 FieldTypeKind::Map { key_ty, val_ty } => {
434 if fa.width.is_some() {
435 return Err(syn::Error::new_spanned(
436 &f.ty,
437 "`width` is not valid on HashMap/BTreeMap fields",
438 ));
439 }
440 if fa.expand {
441 return Err(syn::Error::new_spanned(
442 &f.ty,
443 "`expand`/`unnest` is not valid on HashMap/BTreeMap fields",
444 ));
445 }
446 resolved.push(EnumResolvedField::Map(Box::new(EnumMapFieldData {
447 base_name: col_name_str,
448 binding,
449 rust_name,
450 key_ty: key_ty.clone(),
451 val_ty: val_ty.clone(),
452 map_ty: f.ty.clone(),
453 })));
454 }
455 FieldTypeKind::Struct { inner_ty } => {
456 if fa.width.is_some() {
457 return Err(syn::Error::new_spanned(
458 &f.ty,
459 "`width` is not valid on struct fields; use `#[dataframe(as_list)]` \
460 to keep as an opaque list-column",
461 ));
462 }
463 if fa.expand {
464 return Err(syn::Error::new_spanned(
465 &f.ty,
466 "`expand`/`unnest` is not valid on struct fields; struct fields \
467 flatten by default via their DataFrameRow impl",
468 ));
469 }
470 resolved.push(EnumResolvedField::Struct(Box::new(
471 EnumStructFieldData {
472 base_name: col_name_str,
473 binding,
474 rust_name,
475 inner_ty: inner_ty.clone(),
476 },
477 )));
478 }
479 FieldTypeKind::Scalar => {
480 resolved.push(EnumResolvedField::Single(Box::new(
481 EnumSingleFieldData {
482 col_name: format_ident!("{}", col_name_str),
483 binding,
484 rust_name,
485 ty: f.ty.clone(),
486 needs_into_list: false,
487 is_factor: false,
488 },
489 )));
490 }
491 }
492 }
493 }
494 variant_infos.push(VariantInfo {
495 name: variant.ident.clone(),
496 shape: VariantShape::Tuple,
497 fields: resolved,
498 skipped_fields: vec![],
499 });
500 }
501 Fields::Unit => {
502 variant_infos.push(VariantInfo {
503 name: variant.ident.clone(),
504 shape: VariantShape::Unit,
505 fields: vec![],
506 skipped_fields: vec![],
507 });
508 }
509 }
510 }
511 // endregion
512
513 // region: Resolve unified schema
514 // Collect all unique column names, check type consistency.
515 // Expanded fields contribute multiple columns to the schema.
516 let coerce_to_string = attrs.conflicts.as_deref() == Some("string");
517 let string_ty: syn::Type = syn::parse_quote!(String);
518 let mut registry = ColumnRegistry::new(coerce_to_string, &string_ty);
519
520 for (variant_idx, vi) in variant_infos.iter().enumerate() {
521 for erf in &vi.fields {
522 // Use the rust_name span for error reporting
523 let err_span = erf.rust_name().span();
524 match erf {
525 EnumResolvedField::Single(data) => {
526 if data.is_factor {
527 registry.register_factor(
528 &data.col_name.to_string(),
529 &data.ty,
530 variant_idx,
531 &vi.name,
532 err_span,
533 )?;
534 } else {
535 registry.register(
536 &data.col_name.to_string(),
537 &data.ty,
538 variant_idx,
539 &vi.name,
540 err_span,
541 )?;
542 }
543 }
544 EnumResolvedField::ExpandedFixed(data) => {
545 for i in 1..=data.len {
546 let name = format!("{}_{}", data.base_name, i);
547 registry.register(&name, &data.elem_ty, variant_idx, &vi.name, err_span)?;
548 }
549 }
550 EnumResolvedField::ExpandedVec(data) => {
551 for i in 1..=data.width {
552 let name = format!("{}_{}", data.base_name, i);
553 registry.register(&name, &data.elem_ty, variant_idx, &vi.name, err_span)?;
554 }
555 }
556 // AutoExpandVec: not registered in ColumnRegistry (width is dynamic).
557 // Collected separately below.
558 EnumResolvedField::AutoExpandVec(..) => {}
559 EnumResolvedField::Map(data) => {
560 let key_ty = &data.key_ty;
561 let val_ty = &data.val_ty;
562 let keys_name = format!("{}_keys", data.base_name);
563 let vals_name = format!("{}_values", data.base_name);
564 // Column types are Vec<K> and Vec<V> respectively (used as Vec<Option<Vec<K>>>
565 // / Vec<Option<Vec<V>>> in companion struct via ColumnRegistry wrapping).
566 let key_vec_ty: syn::Type = syn::parse_quote!(Vec<#key_ty>);
567 let val_vec_ty: syn::Type = syn::parse_quote!(Vec<#val_ty>);
568 registry.register(&keys_name, &key_vec_ty, variant_idx, &vi.name, err_span)?;
569 registry.register(&vals_name, &val_vec_ty, variant_idx, &vi.name, err_span)?;
570 }
571 // Struct: registers one Vec<Option<Inner>> column under base_name.
572 // Flattening into prefixed columns happens at into_data_frame() time, not here.
573 EnumResolvedField::Struct(data) => {
574 let inner_ty = &data.inner_ty;
575 // Register as Option<Inner>; the column in the companion struct is Vec<Option<Inner>>.
576 registry.register(
577 &data.base_name,
578 inner_ty,
579 variant_idx,
580 &vi.name,
581 err_span,
582 )?;
583 }
584 }
585 }
586 }
587 let columns = registry.columns;
588 // endregion
589
590 // region: Collect auto-expand fields (per-variant, for split method)
591 struct EnumAutoExpandCol {
592 df_field: syn::Ident,
593 base_name: String,
594 elem_ty: syn::Type,
595 container_ty: syn::Type,
596 present_in: Vec<usize>,
597 }
598
599 let mut auto_expand_cols: Vec<EnumAutoExpandCol> = Vec::new();
600 let mut auto_expand_index: HashMap<String, usize> = HashMap::new();
601
602 for (variant_idx, vi) in variant_infos.iter().enumerate() {
603 for erf in &vi.fields {
604 if let EnumResolvedField::AutoExpandVec(auto_data) = erf {
605 if let Some(&idx) = auto_expand_index.get(&auto_data.base_name) {
606 let elem_match = auto_expand_cols[idx].elem_ty == auto_data.elem_ty;
607 let container_match =
608 auto_expand_cols[idx].container_ty == auto_data.container_ty;
609 if !elem_match {
610 if coerce_to_string {
611 auto_expand_cols[idx].elem_ty = string_ty.clone();
612 } else {
613 return Err(syn::Error::new(
614 auto_data.rust_name.span(),
615 format!(
616 "type conflict for auto-expand field `{}`: different element type \
617 than a previous variant; \
618 use `#[dataframe(conflicts = \"string\")]` to coerce",
619 auto_data.base_name,
620 ),
621 ));
622 }
623 }
624 if !container_match {
625 return Err(syn::Error::new(
626 auto_data.rust_name.span(),
627 format!(
628 "container type mismatch for auto-expand field `{}`: \
629 all variants must use the same container type \
630 (`Vec<T>`, `Box<[T]>`, or `&[T]`)",
631 auto_data.base_name,
632 ),
633 ));
634 }
635 auto_expand_cols[idx].present_in.push(variant_idx);
636 } else {
637 let idx = auto_expand_cols.len();
638 auto_expand_cols.push(EnumAutoExpandCol {
639 df_field: format_ident!("{}", auto_data.base_name),
640 base_name: auto_data.base_name.clone(),
641 elem_ty: auto_data.elem_ty.clone(),
642 container_ty: auto_data.container_ty.clone(),
643 present_in: vec![variant_idx],
644 });
645 auto_expand_index.insert(auto_data.base_name.clone(), idx);
646 }
647 }
648 }
649 }
650 let has_enum_auto_expand = !auto_expand_cols.is_empty();
651 // endregion
652
653 // region: Collect struct fields (for bespoke into_data_frame flatten)
654 struct EnumStructCol {
655 /// Companion struct field name (matches base_name in registry).
656 df_field: syn::Ident,
657 /// Column prefix (same as df_field, used to prefix inner col names).
658 base_name: String,
659 /// Inner type.
660 inner_ty: syn::Type,
661 }
662
663 let mut struct_cols: Vec<EnumStructCol> = Vec::new();
664 let mut struct_col_index: HashMap<String, bool> = HashMap::new();
665
666 for vi in &variant_infos {
667 for erf in &vi.fields {
668 if let EnumResolvedField::Struct(data) = erf
669 && !struct_col_index.contains_key(&data.base_name)
670 {
671 struct_col_index.insert(data.base_name.clone(), true);
672 struct_cols.push(EnumStructCol {
673 df_field: format_ident!("{}", data.base_name),
674 base_name: data.base_name.clone(),
675 inner_ty: data.inner_ty.clone(),
676 });
677 }
678 }
679 }
680 let has_struct_cols = !struct_cols.is_empty();
681 // endregion
682
683 // region: Collect as_list struct fields (Single fields that need per-element into_list())
684 //
685 // These are Single fields with `needs_into_list = true`: struct-typed fields that carry
686 // `#[dataframe(as_list)]`. The companion struct stores `Vec<Option<T>>` (raw Rust struct),
687 // but `into_data_frame` must convert each element via `.into_list()` before building the SEXP.
688 // We collect them so we can:
689 // a) Force the dynamic `into_data_frame` path (they need per-element conversion, not IntoR).
690 // b) Emit the per-element conversion in the dynamic path.
691 let mut as_list_struct_col_names: std::collections::HashSet<String> =
692 std::collections::HashSet::new();
693 for vi in &variant_infos {
694 for erf in &vi.fields {
695 if let EnumResolvedField::Single(data) = erf
696 && data.needs_into_list
697 {
698 as_list_struct_col_names.insert(data.col_name.to_string());
699 }
700 }
701 }
702 let has_as_list_struct_cols = !as_list_struct_col_names.is_empty();
703
704 // Collect factor column names (Single fields with `is_factor = true`).
705 // These are emitted via `FactorOptionVec<T>` wrapping in `into_data_frame`.
706 let mut factor_col_names: std::collections::HashSet<String> = std::collections::HashSet::new();
707 for vi in &variant_infos {
708 for erf in &vi.fields {
709 if let EnumResolvedField::Single(data) = erf
710 && data.is_factor
711 {
712 factor_col_names.insert(data.col_name.to_string());
713 }
714 }
715 }
716 let has_factor_cols = !factor_col_names.is_empty();
717 // endregion
718
719 // region: Generate companion struct
720 let has_tag = attrs.tag.is_some();
721
722 let tag_field = if has_tag {
723 quote! { pub _tag: Vec<String>, }
724 } else {
725 TokenStream::new()
726 };
727
728 let mut df_fields: Vec<TokenStream> = columns
729 .iter()
730 .map(|col| {
731 let name = &col.col_name;
732 let ty = &col.ty;
733 quote! { pub #name: Vec<Option<#ty>> }
734 })
735 .collect();
736 // Auto-expand fields: Vec<Option<ContainerType>>
737 for ac in &auto_expand_cols {
738 let name = &ac.df_field;
739 let cty = &ac.container_ty;
740 df_fields.push(quote! { pub #name: Vec<Option<#cty>> });
741 }
742
743 // When the companion struct would otherwise have no fields (unit-only enum,
744 // no tag) but has generic type parameters, emit a PhantomData field to keep
745 // the type parameter in scope — without it the struct is E0392 (unused param).
746 let has_any_field = has_tag || !df_fields.is_empty();
747 let phantom_field = if !has_any_field && !impl_generics.to_token_stream().is_empty() {
748 let type_params: Vec<_> = input.generics.type_params().map(|tp| &tp.ident).collect();
749 if !type_params.is_empty() {
750 quote! {
751 #[allow(dead_code)]
752 _phantom: ::std::marker::PhantomData<(#(#type_params,)*)>,
753 }
754 } else {
755 TokenStream::new()
756 }
757 } else {
758 TokenStream::new()
759 };
760
761 let dataframe_struct = quote! {
762 #[derive(Debug, Clone)]
763 pub struct #df_name #impl_generics #where_clause {
764 #tag_field
765 #(#df_fields),*
766 #phantom_field
767 }
768 };
769 // endregion
770
771 // region: Generate IntoDataFrame
772 // The first "real" column for length reference. If tag exists, use _tag.
773 let length_ref = if has_tag {
774 quote! { self._tag.len() }
775 } else if let Some(first_col) = columns.first() {
776 let first = &first_col.col_name;
777 quote! { self.#first.len() }
778 } else if let Some(first_ac) = auto_expand_cols.first() {
779 let first = &first_ac.df_field;
780 quote! { self.#first.len() }
781 } else {
782 // No columns and no tag — degenerate case, length is 0
783 quote! { 0usize }
784 };
785
786 // Each pair protects its SEXP via `__scope.protect_raw` so previously-built
787 // column SEXPs survive subsequent column allocations. Pre-fix the raw
788 // `vec![(name, into_sexp(...)), ...]` left every SEXP unrooted across the
789 // next column's allocations — UAF under gctorture
790 // (reviews/2026-05-07-gctorture-audit.md).
791 let tag_pair = if let Some(ref tag_name) = attrs.tag {
792 quote! { (#tag_name, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self._tag))), }
793 } else {
794 TokenStream::new()
795 };
796
797 let col_pairs: Vec<TokenStream> = columns
798 .iter()
799 .map(|col| {
800 let name = &col.col_name;
801 let name_str = name.to_string();
802 quote! { (#name_str, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self.#name))) }
803 })
804 .collect();
805
806 // Length checks for all columns
807 let length_checks: Vec<TokenStream> = columns
808 .iter()
809 .map(|col| {
810 let name = &col.col_name;
811 let name_str = name.to_string();
812 quote! {
813 assert!(
814 self.#name.len() == _n_rows,
815 "column length mismatch in {}: column `{}` has length {} but expected {}",
816 stringify!(#df_name),
817 #name_str,
818 self.#name.len(),
819 _n_rows,
820 );
821 }
822 })
823 .collect();
824
825 // Build the set of column names that are struct-col placeholders (to skip in normal push).
826 let struct_col_names: std::collections::HashSet<String> =
827 struct_cols.iter().map(|sc| sc.base_name.clone()).collect();
828
829 let into_dataframe_impl = if has_enum_auto_expand
830 || has_struct_cols
831 || has_as_list_struct_cols
832 || has_factor_cols
833 {
834 // Dynamic pair building for auto-expand, struct fields, as_list struct fields,
835 // and/or as_factor fields.
836 let tag_push_pair = if let Some(ref tag_name) = attrs.tag {
837 quote! {
838 __df_pairs.push((
839 #tag_name.to_string(),
840 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self._tag)),
841 ));
842 }
843 } else {
844 TokenStream::new()
845 };
846
847 // Static columns — skip struct-col placeholders (handled in flatten block below),
848 // as-list struct fields (handled in the per-element conversion block below),
849 // and factor columns (handled in the FactorOptionVec wrapping block below).
850 let static_pair_pushes: Vec<TokenStream> = columns
851 .iter()
852 .filter(|col| {
853 let name_str = col.col_name.to_string();
854 !struct_col_names.contains(&name_str)
855 && !as_list_struct_col_names.contains(&name_str)
856 && !factor_col_names.contains(&name_str)
857 })
858 .map(|col| {
859 let name = &col.col_name;
860 let name_str = name.to_string();
861 quote! {
862 __df_pairs.push((
863 #name_str.to_string(),
864 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(self.#name)),
865 ));
866 }
867 })
868 .collect();
869
870 // as_list struct fields: convert each element via into_list() at conversion time
871 // (not during row accumulation), producing a VECSXP list-column with NULL for absent rows.
872 let as_list_struct_pushes: Vec<TokenStream> = columns
873 .iter()
874 .filter(|col| as_list_struct_col_names.contains(&col.col_name.to_string()))
875 .map(|col| {
876 let name = &col.col_name;
877 let name_str = name.to_string();
878 let ty = &col.ty;
879 quote! {
880 {
881 // Map Vec<Option<T>> → Vec<Option<List>> then convert to SEXP.
882 // This is the only R-touching operation for as_list struct fields.
883 let __as_list_col: Vec<Option<::miniextendr_api::list::List>> =
884 self.#name
885 .into_iter()
886 .map(|__opt: Option<#ty>| {
887 __opt.map(|v| ::miniextendr_api::list::IntoList::into_list(v))
888 })
889 .collect();
890 __df_pairs.push((
891 #name_str.to_string(),
892 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(__as_list_col)),
893 ));
894 }
895 }
896 })
897 .collect();
898
899 // as_factor columns: wrap Vec<Option<T>> in FactorOptionVec<T> before calling into_sexp.
900 // Uses the UnitEnumFactor blanket impl: impl<T: UnitEnumFactor> IntoR for FactorOptionVec<T>.
901 let factor_pair_pushes: Vec<TokenStream> = columns
902 .iter()
903 .filter(|col| factor_col_names.contains(&col.col_name.to_string()))
904 .map(|col| {
905 let name = &col.col_name;
906 let name_str = name.to_string();
907 let ty = &col.ty;
908 quote! {
909 __df_pairs.push((
910 #name_str.to_string(),
911 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(
912 ::miniextendr_api::factor::FactorOptionVec::<#ty>::from(self.#name)
913 )),
914 ));
915 }
916 })
917 .collect();
918
919 let auto_expand_pair_pushes: Vec<TokenStream> = auto_expand_cols
920 .iter()
921 .map(|ac| {
922 let df_field = &ac.df_field;
923 let base_name_str = &ac.base_name;
924 let elem_ty = &ac.elem_ty;
925 quote! {
926 {
927 let __auto = self.#df_field;
928 let __max = __auto.iter()
929 .filter_map(|v| v.as_ref())
930 .map(|v| v.len())
931 .max()
932 .unwrap_or(0);
933 let mut __cols: Vec<Vec<Option<#elem_ty>>> = (0..__max)
934 .map(|_| Vec::with_capacity(_n_rows))
935 .collect();
936 for __opt_vec in &__auto {
937 for (__i, __col) in __cols.iter_mut().enumerate() {
938 __col.push(
939 __opt_vec.as_ref().and_then(|v| v.get(__i).cloned()),
940 );
941 }
942 }
943 for (__i, __col) in __cols.into_iter().enumerate() {
944 __df_pairs.push((
945 format!("{}_{}", #base_name_str, __i + 1),
946 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(__col)),
947 ));
948 }
949 }
950 }
951 })
952 .collect();
953
954 // Struct field flatten blocks: for each Vec<Option<Inner>> column, collect present
955 // rows into a dense Vec<Inner>, track presence indices, call Inner::to_dataframe,
956 // extract named columns via into_named_columns(), scatter them to full row count
957 // with None-fill, and push with prefixed names.
958 let struct_flatten_pushes: Vec<TokenStream> = struct_cols
959 .iter()
960 .map(|sc| {
961 let df_field = &sc.df_field;
962 let base_name_str = &sc.base_name;
963 let inner_ty = &sc.inner_ty;
964 quote! {
965 {
966 // Separate the Some/None rows — collect present rows densely
967 // (no Clone needed: we consume the Vec<Option<Inner>>).
968 let mut __present_idx: Vec<usize> = Vec::new();
969 let mut __inner_rows: Vec<#inner_ty> = Vec::new();
970 for (__row_i, __opt) in self.#df_field.into_iter().enumerate() {
971 if let Some(__inner) = __opt {
972 __present_idx.push(__row_i);
973 __inner_rows.push(__inner);
974 }
975 }
976 // Call Inner::to_dataframe and extract named column SEXPs.
977 let __inner_df = <#inner_ty>::to_dataframe(__inner_rows);
978 // into_named_columns consumes __inner_df and returns (name, SEXP) pairs.
979 let __inner_cols = ::miniextendr_api::convert::ColumnSource::into_named_columns(__inner_df);
980 // Scatter each column back to full _n_rows with NA/NULL-fill,
981 // preserving the source column's SEXPTYPE.
982 for (__inner_col_name, __inner_col_sexp) in __inner_cols {
983 // Protect the source column across the scatter allocation.
984 let __src = __scope.protect_raw(__inner_col_sexp);
985 let __prefixed = format!("{}_{}", #base_name_str, __inner_col_name);
986 let __scattered = unsafe {
987 let __out = ::miniextendr_api::convert::scatter_column(
988 __src,
989 &__present_idx,
990 _n_rows,
991 );
992 __scope.protect_raw(__out)
993 };
994 __df_pairs.push((__prefixed, __scattered));
995 }
996 }
997 }
998 })
999 .collect();
1000
1001 quote! {
1002 impl #impl_generics ::miniextendr_api::convert::ColumnSource for #df_name #ty_generics #where_clause {
1003 fn into_column_list(self) -> ::miniextendr_api::List {
1004 let _n_rows = #length_ref;
1005 #(#length_checks)*
1006 // SAFETY: into_column_list only runs on the R main thread.
1007 // ProtectScope keeps each column SEXP rooted across the
1008 // next column's allocations; from_raw_pairs writes them
1009 // into the parent VECSXP before we drop the scope.
1010 unsafe {
1011 let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
1012 let mut __df_pairs: Vec<(
1013 String,
1014 ::miniextendr_api::SEXP,
1015 )> = Vec::new();
1016 #tag_push_pair
1017 #(#static_pair_pushes)*
1018 #(#factor_pair_pushes)*
1019 #(#auto_expand_pair_pushes)*
1020 #(#struct_flatten_pushes)*
1021 #(#as_list_struct_pushes)*
1022 ::miniextendr_api::list::List::from_raw_pairs(__df_pairs)
1023 .set_class_str(&["data.frame"])
1024 .set_row_names_int(_n_rows)
1025 }
1026 }
1027 }
1028 }
1029 } else {
1030 quote! {
1031 impl #impl_generics ::miniextendr_api::convert::ColumnSource for #df_name #ty_generics #where_clause {
1032 fn into_column_list(self) -> ::miniextendr_api::List {
1033 let _n_rows = #length_ref;
1034 #(#length_checks)*
1035 // SAFETY: see auto-expand branch.
1036 unsafe {
1037 let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
1038 // Explicit type annotation so the vec![] case (unit-only enum
1039 // with no columns and no tag) doesn't hit E0282 inference failure.
1040 let __pairs: Vec<(&str, ::miniextendr_api::SEXP)> = vec![
1041 #tag_pair
1042 #(#col_pairs),*
1043 ];
1044 ::miniextendr_api::list::List::from_raw_pairs(__pairs)
1045 .set_class_str(&["data.frame"])
1046 .set_row_names_int(_n_rows)
1047 }
1048 }
1049 }
1050 }
1051 };
1052
1053 // Compile-time assertions: one per struct field, asserting the inner type
1054 // implements DataFrameRow.
1055 let struct_assertions: Vec<TokenStream> = struct_cols
1056 .iter()
1057 .map(|sc| {
1058 let inner_ty = &sc.inner_ty;
1059 quote! {
1060 const _: () = {
1061 fn _assert_inner_is_dataframe_row<T: ::miniextendr_api::markers::DataFrameRow>() {}
1062 fn _do_assert #impl_generics () #where_clause {
1063 _assert_inner_is_dataframe_row::<#inner_ty>();
1064 }
1065 };
1066 }
1067 })
1068 .collect();
1069
1070 // Payload collision assertions (#486): one per nested-enum struct field.
1071 // For each `kind: Inner` field, emit:
1072 // const _: () = ::miniextendr_api::markers::assert_no_payload_field_collision(
1073 // <Inner as DataFramePayloadFields>::FIELDS,
1074 // <Inner as DataFramePayloadFields>::TAG,
1075 // );
1076 // This fires a compile-time panic if any inner payload field name equals the
1077 // inner enum's own tag suffix, which would (after outer prefix expansion) produce
1078 // a column name identical to the outer discriminant column.
1079 let payload_collision_assertions: Vec<TokenStream> = struct_cols
1080 .iter()
1081 .map(|sc| {
1082 let inner_ty = &sc.inner_ty;
1083 quote! {
1084 const _: () = ::miniextendr_api::markers::assert_no_payload_field_collision(
1085 <#inner_ty as ::miniextendr_api::markers::DataFramePayloadFields>::FIELDS,
1086 <#inner_ty as ::miniextendr_api::markers::DataFramePayloadFields>::TAG,
1087 );
1088 }
1089 })
1090 .collect();
1091
1092 // Sibling collision assertions (#544): one per nested-enum struct field.
1093 //
1094 // The B1 parse-time check (earlier in this function) hardcodes `"variant"` as the
1095 // inner tag when building the discriminant column name to compare against sibling
1096 // Single fields. That check covers the common case with better spans/messages.
1097 //
1098 // This const assertion covers the non-default-tag case: when `Inner` uses
1099 // `#[dataframe(tag = "foo")]`, the discriminant column is `<base>_foo`, not
1100 // `<base>_variant`. The const assertion uses `<Inner as DataFramePayloadFields>::TAG`
1101 // so it resolves to the actual tag at compile time regardless of the value.
1102 //
1103 // We collect all Single col names across ALL variants (not just the variant that
1104 // introduced the struct field) — a collision in any one variant is a bug.
1105 let all_single_col_names: Vec<String> = {
1106 let mut seen = std::collections::HashSet::new();
1107 let mut names = Vec::new();
1108 for vi in &variant_infos {
1109 for erf in &vi.fields {
1110 if let EnumResolvedField::Single(d) = erf {
1111 let col = d.col_name.to_string();
1112 if seen.insert(col.clone()) {
1113 names.push(col);
1114 }
1115 }
1116 }
1117 }
1118 names
1119 };
1120 let sibling_collision_assertions: Vec<TokenStream> = struct_cols
1121 .iter()
1122 .map(|sc| {
1123 let inner_ty = &sc.inner_ty;
1124 let base_str = &sc.base_name;
1125 let sibling_lits = all_single_col_names
1126 .iter()
1127 .map(|s| quote! { #s })
1128 .collect::<Vec<_>>();
1129 quote! {
1130 const _: () = ::miniextendr_api::markers::assert_no_sibling_field_collision(
1131 &[#(#sibling_lits),*],
1132 #base_str,
1133 <#inner_ty as ::miniextendr_api::markers::DataFramePayloadFields>::TAG,
1134 );
1135 }
1136 })
1137 .collect();
1138 // endregion
1139
1140 // region: Generate From<Vec<Enum>>
1141 let mut col_vec_inits: Vec<TokenStream> = columns
1142 .iter()
1143 .map(|col| {
1144 let name = &col.col_name;
1145 let ty = &col.ty;
1146 quote! { let mut #name: Vec<Option<#ty>> = Vec::with_capacity(len); }
1147 })
1148 .collect();
1149 for ac in &auto_expand_cols {
1150 let name = &ac.df_field;
1151 let cty = &ac.container_ty;
1152 col_vec_inits.push(quote! { let mut #name: Vec<Option<#cty>> = Vec::with_capacity(len); });
1153 }
1154
1155 let tag_init = if has_tag {
1156 quote! { let mut _tag: Vec<String> = Vec::with_capacity(len); }
1157 } else {
1158 TokenStream::new()
1159 };
1160
1161 // Build match arms for each variant
1162 let match_arms: Vec<TokenStream> = variant_infos
1163 .iter()
1164 .enumerate()
1165 .map(|(variant_idx, vi)| {
1166 let variant_name = &vi.name;
1167 let variant_name_str = variant_name.to_string();
1168
1169 let tag_push = if has_tag {
1170 quote! { _tag.push(#variant_name_str.to_string()); }
1171 } else {
1172 TokenStream::new()
1173 };
1174
1175 // Build push statements for each schema column.
1176 // For present columns: push Some(value), for absent: push None.
1177 // Expanded fields contribute multiple columns from one binding.
1178
1179 // First, build a map of which schema columns this variant contributes to.
1180 let col_pushes: Vec<TokenStream> = columns
1181 .iter()
1182 .map(|col| {
1183 let col_name = &col.col_name;
1184 if col.present_in.contains(&variant_idx) {
1185 let col_name_str = col_name.to_string();
1186
1187 for erf in &vi.fields {
1188 match erf {
1189 EnumResolvedField::Single(data)
1190 if data.col_name == *col_name =>
1191 {
1192 let binding = &data.binding;
1193 if col.string_coerced {
1194 return quote! { #col_name.push(Some(ToString::to_string(&#binding))); };
1195 } else {
1196 return quote! { #col_name.push(Some(#binding)); };
1197 }
1198 }
1199 EnumResolvedField::ExpandedFixed(data) => {
1200 for i in 1..=data.len {
1201 let expanded_name = format!("{}_{}", data.base_name, i);
1202 if expanded_name == col_name_str {
1203 let binding = &data.binding;
1204 let idx = syn::Index::from(i - 1);
1205 return quote! { #col_name.push(Some(#binding[#idx])); };
1206 }
1207 }
1208 }
1209 EnumResolvedField::ExpandedVec(data) => {
1210 for i in 1..=data.width {
1211 let expanded_name = format!("{}_{}", data.base_name, i);
1212 if expanded_name == col_name_str {
1213 let binding = &data.binding;
1214 let get_idx = i - 1;
1215 return quote! { #col_name.push(#binding.get(#get_idx).cloned()); };
1216 }
1217 }
1218 }
1219 EnumResolvedField::Map(data) => {
1220 let keys_name = format!("{}_keys", data.base_name);
1221 let vals_name = format!("{}_values", data.base_name);
1222 let binding = &data.binding;
1223 // Use unzip() to guarantee pairwise alignment of keys and values.
1224 // Both columns are emitted together when the _keys column is
1225 // processed; the _values column is skipped (already handled).
1226 if col_name_str == keys_name {
1227 let vals_col = format_ident!("{}", vals_name);
1228 return quote! {
1229 let (__mx_keys, __mx_vals) = #binding.into_iter().unzip::<_, _, Vec<_>, Vec<_>>();
1230 #col_name.push(Some(__mx_keys));
1231 #vals_col.push(Some(__mx_vals));
1232 };
1233 }
1234 if col_name_str == vals_name {
1235 // Already handled when keys col was processed; emit no-op.
1236 return quote! {};
1237 }
1238 }
1239 // Struct field: push Some(binding) to the Vec<Option<Inner>> column.
1240 EnumResolvedField::Struct(data)
1241 if data.base_name == col_name_str =>
1242 {
1243 let binding = &data.binding;
1244 return quote! { #col_name.push(Some(#binding)); };
1245 }
1246 // AutoExpandVec doesn't contribute to static columns
1247 _ => {}
1248 }
1249 }
1250 quote! { #col_name.push(None); }
1251 } else {
1252 quote! { #col_name.push(None); }
1253 }
1254 })
1255 .collect();
1256
1257 // Auto-expand push statements
1258 let auto_expand_pushes: Vec<TokenStream> = auto_expand_cols
1259 .iter()
1260 .map(|ac| {
1261 let ac_field = &ac.df_field;
1262 if ac.present_in.contains(&variant_idx) {
1263 // Find the binding for this auto-expand field
1264 for erf in &vi.fields {
1265 if let EnumResolvedField::AutoExpandVec(data) = erf
1266 && data.base_name == ac.base_name
1267 {
1268 let binding = &data.binding;
1269 return quote! { #ac_field.push(Some(#binding)); };
1270 }
1271 }
1272 // shouldn't reach here
1273 quote! { #ac_field.push(None); }
1274 } else {
1275 quote! { #ac_field.push(None); }
1276 }
1277 })
1278 .collect();
1279
1280 // Generate destructure pattern based on variant shape
1281 match vi.shape {
1282 VariantShape::Named => {
1283 let mut field_bindings: Vec<TokenStream> = vi.fields.iter().map(|erf| {
1284 let rust_name = erf.rust_name();
1285 let binding = erf.binding();
1286 quote! { #rust_name: #binding }
1287 }).collect();
1288 // Add skipped fields as wildcard bindings
1289 for skipped in &vi.skipped_fields {
1290 field_bindings.push(quote! { #skipped: _ });
1291 }
1292 quote! {
1293 #row_name::#variant_name { #(#field_bindings),* } => {
1294 #tag_push
1295 #(#col_pushes)*
1296 #(#auto_expand_pushes)*
1297 }
1298 }
1299 }
1300 VariantShape::Tuple => {
1301 let field_bindings: Vec<TokenStream> = vi.fields.iter().map(|erf| {
1302 let binding = erf.binding();
1303 quote! { #binding }
1304 }).collect();
1305 quote! {
1306 #row_name::#variant_name(#(#field_bindings),*) => {
1307 #tag_push
1308 #(#col_pushes)*
1309 #(#auto_expand_pushes)*
1310 }
1311 }
1312 }
1313 VariantShape::Unit => {
1314 quote! {
1315 #row_name::#variant_name => {
1316 #tag_push
1317 #(#col_pushes)*
1318 #(#auto_expand_pushes)*
1319 }
1320 }
1321 }
1322 }
1323 })
1324 .collect();
1325
1326 let tag_struct_field = if has_tag {
1327 quote! { _tag, }
1328 } else {
1329 TokenStream::new()
1330 };
1331
1332 let mut col_struct_fields: Vec<TokenStream> = columns
1333 .iter()
1334 .map(|col| {
1335 let name = &col.col_name;
1336 quote! { #name }
1337 })
1338 .collect();
1339 for ac in &auto_expand_cols {
1340 let name = &ac.df_field;
1341 col_struct_fields.push(quote! { #name });
1342 }
1343
1344 // Struct literal initializer for the PhantomData field, when emitted.
1345 //
1346 // `phantom_field` is:
1347 // - Empty when the companion struct has at least one real field (tag or
1348 // column), or when there are no generic type parameters (const-param
1349 // enums don't need PhantomData — Rust allows unused const params).
1350 // - Non-empty only when the struct would otherwise have *zero* fields AND
1351 // the enum carries at least one type parameter `T`, where the generated
1352 // `PhantomData<T>` field prevents E0392 ("unused type parameter") on the
1353 // companion struct. In practice this path is only reachable if the user
1354 // somehow has a type-generic unit-only enum; Rust's own E0392 rule blocks
1355 // such enums at the user-definition level, so this branch is a defensive
1356 // guard for hypothetical macro-generated enum inputs.
1357 let phantom_struct_field_init = if phantom_field.is_empty() {
1358 TokenStream::new()
1359 } else {
1360 quote! { _phantom: ::std::marker::PhantomData, }
1361 };
1362
1363 let from_vec_impl = quote! {
1364 impl #impl_generics From<Vec<#row_name #ty_generics>> for #df_name #ty_generics #where_clause {
1365 fn from(rows: Vec<#row_name #ty_generics>) -> Self {
1366 let len = rows.len();
1367 #tag_init
1368 #(#col_vec_inits)*
1369 for row in rows {
1370 match row {
1371 #(#match_arms)*
1372 }
1373 }
1374 #df_name {
1375 #tag_struct_field
1376 #(#col_struct_fields),*
1377 #phantom_struct_field_init
1378 }
1379 }
1380 }
1381 };
1382 // endregion
1383
1384 // region: Generate from_rows_par (parallel scatter-write via ColumnWriter)
1385 let from_rows_par_method = if !columns.is_empty() || !auto_expand_cols.is_empty() || has_tag {
1386 // Column declarations
1387 let mut par_col_decls = Vec::new();
1388 if has_tag {
1389 par_col_decls.push(quote! {
1390 let mut _tag: Vec<String> = vec![String::new(); len];
1391 });
1392 }
1393 for col in &columns {
1394 let name = &col.col_name;
1395 let ty = &col.ty;
1396 par_col_decls.push(quote! {
1397 let mut #name: Vec<Option<#ty>> = vec![None; len];
1398 });
1399 }
1400 for ac in &auto_expand_cols {
1401 let name = &ac.df_field;
1402 let cty = &ac.container_ty;
1403 par_col_decls.push(quote! {
1404 let mut #name: Vec<Option<#cty>> = vec![None; len];
1405 });
1406 }
1407
1408 // Writer declarations
1409 let mut writer_decls = Vec::new();
1410 if has_tag {
1411 writer_decls.push(quote! {
1412 let __w_tag = unsafe {
1413 ::miniextendr_api::rayon_bridge::ColumnWriter::new(&mut _tag)
1414 };
1415 });
1416 }
1417 for col in &columns {
1418 let name = &col.col_name;
1419 let w_name = format_ident!("__w_{}", name);
1420 writer_decls.push(quote! {
1421 let #w_name = unsafe {
1422 ::miniextendr_api::rayon_bridge::ColumnWriter::new(&mut #name)
1423 };
1424 });
1425 }
1426 for ac in &auto_expand_cols {
1427 let name = &ac.df_field;
1428 let w_name = format_ident!("__w_{}", name);
1429 writer_decls.push(quote! {
1430 let #w_name = unsafe {
1431 ::miniextendr_api::rayon_bridge::ColumnWriter::new(&mut #name)
1432 };
1433 });
1434 }
1435
1436 // Match arms for parallel path
1437 let par_match_arms: Vec<TokenStream> = variant_infos
1438 .iter()
1439 .enumerate()
1440 .map(|(variant_idx, vi)| {
1441 let variant_name = &vi.name;
1442 let variant_name_str = variant_name.to_string();
1443
1444 let tag_write = if has_tag {
1445 quote! { __w_tag.write(__i, #variant_name_str.to_string()); }
1446 } else {
1447 TokenStream::new()
1448 };
1449
1450 // Write calls for schema columns
1451 let col_writes: Vec<TokenStream> = columns
1452 .iter()
1453 .map(|col| {
1454 let col_name = &col.col_name;
1455 let w_name = format_ident!("__w_{}", col_name);
1456 if col.present_in.contains(&variant_idx) {
1457 let col_name_str = col_name.to_string();
1458 for erf in &vi.fields {
1459 match erf {
1460 EnumResolvedField::Single(data)
1461 if data.col_name == *col_name =>
1462 {
1463 let binding = &data.binding;
1464 if col.string_coerced {
1465 return quote! { #w_name.write(__i, Some(ToString::to_string(&#binding))); };
1466 } else {
1467 return quote! { #w_name.write(__i, Some(#binding)); };
1468 }
1469 }
1470 EnumResolvedField::ExpandedFixed(data) => {
1471 for i in 1..=data.len {
1472 let expanded_name = format!("{}_{}", data.base_name, i);
1473 if expanded_name == col_name_str {
1474 let binding = &data.binding;
1475 let idx = syn::Index::from(i - 1);
1476 return quote! { #w_name.write(__i, Some(#binding[#idx])); };
1477 }
1478 }
1479 }
1480 EnumResolvedField::ExpandedVec(data) => {
1481 for i in 1..=data.width {
1482 let expanded_name = format!("{}_{}", data.base_name, i);
1483 if expanded_name == col_name_str {
1484 let binding = &data.binding;
1485 let get_idx = i - 1;
1486 return quote! { #w_name.write(__i, #binding.get(#get_idx).cloned()); };
1487 }
1488 }
1489 }
1490 EnumResolvedField::Map(data) => {
1491 let keys_name = format!("{}_keys", data.base_name);
1492 let vals_name = format!("{}_values", data.base_name);
1493 let binding = &data.binding;
1494 // Combined unzip: emit both key and value writes when the
1495 // keys column is processed; skip the values column (handled here).
1496 if col_name_str == keys_name {
1497 let vals_col = format_ident!("{}", vals_name);
1498 let w_vals = format_ident!("__w_{}", vals_col);
1499 return quote! {
1500 let (__mx_keys, __mx_vals) = #binding.into_iter().unzip::<_, _, Vec<_>, Vec<_>>();
1501 #w_name.write(__i, Some(__mx_keys));
1502 #w_vals.write(__i, Some(__mx_vals));
1503 };
1504 }
1505 if col_name_str == vals_name {
1506 // Already handled when keys col was processed.
1507 return quote! {};
1508 }
1509 }
1510 // Struct field: write Some(binding) to Vec<Option<Inner>>.
1511 EnumResolvedField::Struct(data)
1512 if data.base_name == col_name_str =>
1513 {
1514 let binding = &data.binding;
1515 return quote! { #w_name.write(__i, Some(#binding)); };
1516 }
1517 _ => {}
1518 }
1519 }
1520 quote! { #w_name.write(__i, None); }
1521 } else {
1522 quote! { #w_name.write(__i, None); }
1523 }
1524 })
1525 .collect();
1526
1527 // Auto-expand write calls
1528 let auto_expand_writes: Vec<TokenStream> = auto_expand_cols
1529 .iter()
1530 .map(|ac| {
1531 let w_name = format_ident!("__w_{}", ac.df_field);
1532 if ac.present_in.contains(&variant_idx) {
1533 for erf in &vi.fields {
1534 if let EnumResolvedField::AutoExpandVec(data) = erf
1535 && data.base_name == ac.base_name
1536 {
1537 let binding = &data.binding;
1538 return quote! { #w_name.write(__i, Some(#binding)); };
1539 }
1540 }
1541 quote! { #w_name.write(__i, None); }
1542 } else {
1543 quote! { #w_name.write(__i, None); }
1544 }
1545 })
1546 .collect();
1547
1548 // Generate destructure pattern based on variant shape
1549 match vi.shape {
1550 VariantShape::Named => {
1551 let mut field_bindings: Vec<TokenStream> = vi.fields.iter().map(|erf| {
1552 let rust_name = erf.rust_name();
1553 let binding = erf.binding();
1554 quote! { #rust_name: #binding }
1555 }).collect();
1556 for skipped in &vi.skipped_fields {
1557 field_bindings.push(quote! { #skipped: _ });
1558 }
1559 quote! {
1560 #row_name::#variant_name { #(#field_bindings),* } => {
1561 #tag_write
1562 #(#col_writes)*
1563 #(#auto_expand_writes)*
1564 }
1565 }
1566 }
1567 VariantShape::Tuple => {
1568 let field_bindings: Vec<TokenStream> = vi.fields.iter().map(|erf| {
1569 let binding = erf.binding();
1570 quote! { #binding }
1571 }).collect();
1572 quote! {
1573 #row_name::#variant_name(#(#field_bindings),*) => {
1574 #tag_write
1575 #(#col_writes)*
1576 #(#auto_expand_writes)*
1577 }
1578 }
1579 }
1580 VariantShape::Unit => {
1581 quote! {
1582 #row_name::#variant_name => {
1583 #tag_write
1584 #(#col_writes)*
1585 #(#auto_expand_writes)*
1586 }
1587 }
1588 }
1589 }
1590 })
1591 .collect();
1592
1593 // Return struct fields
1594 let par_tag_field = if has_tag {
1595 quote! { _tag, }
1596 } else {
1597 TokenStream::new()
1598 };
1599 let mut par_struct_fields: Vec<TokenStream> = columns
1600 .iter()
1601 .map(|col| {
1602 let name = &col.col_name;
1603 quote! { #name }
1604 })
1605 .collect();
1606 for ac in &auto_expand_cols {
1607 let name = &ac.df_field;
1608 par_struct_fields.push(quote! { #name });
1609 }
1610
1611 quote! {
1612 // api-side rayon gate (#1117): the cfg lives on miniextendr-api's
1613 // macro, not stamped into the consumer crate.
1614 ::miniextendr_api::__dataframe_row_when_rayon! {
1615 // Parallel override of `ColumnarFrame::from_rows_par` (rayon scatter-write).
1616 #[allow(clippy::uninit_vec)]
1617 fn from_rows_par(rows: ::std::vec::Vec<#row_name #ty_generics>) -> Self {
1618 use ::miniextendr_api::rayon_bridge::rayon::prelude::*;
1619 ::miniextendr_api::optionals::parallel::ensure_pool();
1620 let len = rows.len();
1621 #(#par_col_decls)*
1622 {
1623 #(#writer_decls)*
1624 rows.into_par_iter().enumerate().for_each(|(__i, __row)| unsafe {
1625 match __row {
1626 #(#par_match_arms)*
1627 }
1628 });
1629 }
1630 #df_name { #par_tag_field #(#par_struct_fields),* }
1631 }
1632 }
1633 }
1634 } else {
1635 TokenStream::new()
1636 };
1637 // endregion
1638
1639 // region: ColumnarFrame impl on the enum companion (from_rows / from_rows_par)
1640 //
1641 // The pure-Rust columnar verbs live on the `ColumnarFrame` trait. `into_rows` (the
1642 // provided, `IntoIterator`-bounded default) is not callable on enum companions — they
1643 // have no `IntoIterator` and are read back across the R boundary via `FromDataFrame` /
1644 // the one-call `try_from_dataframe`.
1645 let df_methods = quote! {
1646 impl #impl_generics ::miniextendr_api::dataframe::ColumnarFrame<#row_name #ty_generics>
1647 for #df_name #ty_generics #where_clause
1648 {
1649 fn from_rows(rows: ::std::vec::Vec<#row_name #ty_generics>) -> Self {
1650 rows.into()
1651 }
1652
1653 #from_rows_par_method
1654 }
1655 };
1656 // endregion
1657
1658 // region: enum reader (#807) — computed here so `row_methods` can embed the methods
1659 //
1660 // Build the enum reader methods (if this enum is reader-capable). Must happen before
1661 // `row_methods` since `row_methods` embeds the reader methods inline.
1662 let enum_reader_early = build_enum_reader(
1663 row_name,
1664 &variant_infos,
1665 &columns,
1666 attrs,
1667 &impl_generics,
1668 &ty_generics,
1669 where_clause,
1670 );
1671 // endregion
1672
1673 // region: Generate associated methods
1674 let enum_reader_methods = enum_reader_early.clone().unwrap_or_default();
1675 // The documented rows→companion verb is `ColumnarFrame::from_rows` / `Vec<Row>: Into`.
1676 // `to_dataframe` is retained `#[doc(hidden)]` because the struct-flatten / nested-enum
1677 // write paths build a nested companion via `Inner::to_dataframe(..)` without naming the
1678 // inner companion type. The enum readers ride here too (when reader-capable).
1679 let row_methods = quote! {
1680 impl #impl_generics #row_name #ty_generics #where_clause {
1681 #[doc(hidden)]
1682 pub fn to_dataframe(rows: Vec<Self>) -> #df_name #ty_generics {
1683 rows.into()
1684 }
1685
1686 #enum_reader_methods
1687 }
1688 };
1689
1690 // No IntoList assertion for align enums — they go through the companion struct path,
1691 // not the `DataFrame<T>` path, so IntoList is not required.
1692
1693 // region: Generate the split representation (hidden to_dataframe_split + DataFrameRowSplit)
1694 let split_method = generate_split_method(
1695 row_name,
1696 &variant_infos,
1697 &impl_generics,
1698 &ty_generics,
1699 where_clause,
1700 );
1701 // endregion
1702
1703 // Marker trait impl: row type implements DataFrameRow via IntoDataFrame chain.
1704 // This is the impl the compile-time assertion checks for struct-typed variant fields.
1705 let marker_impl = quote! {
1706 impl #impl_generics ::miniextendr_api::markers::DataFrameRow
1707 for #row_name #ty_generics #where_clause {}
1708 };
1709
1710 // DataFramePayloadFields impl (#486): exposes FIELDS (all resolved column names,
1711 // deduplicated) and TAG for compile-time collision detection by outer enums.
1712 // FIELDS lists every single-column payload field name across all variants.
1713 // TAG is the inner enum's #[dataframe(tag = "...")] value (or "" if absent).
1714 let payload_fields_impl = {
1715 // Collect unique field names from all variant payload fields (single columns only).
1716 // We skip expanded (fixed/vec) and struct fields — only direct column contributions.
1717 let mut field_names: Vec<String> = Vec::new();
1718 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1719 for vi in &variant_infos {
1720 for erf in &vi.fields {
1721 if let EnumResolvedField::Single(data) = erf {
1722 let name = data.col_name.to_string();
1723 if seen.insert(name.clone()) {
1724 field_names.push(name);
1725 }
1726 }
1727 }
1728 }
1729 let tag_str = attrs.tag.as_deref().unwrap_or("");
1730 quote! {
1731 impl #impl_generics ::miniextendr_api::markers::DataFramePayloadFields
1732 for #row_name #ty_generics #where_clause
1733 {
1734 const FIELDS: &'static [&'static str] = &[#(#field_names),*];
1735 const TAG: &'static str = #tag_str;
1736 }
1737 }
1738 };
1739
1740 // region: unit-only enum factor impls
1741 // For a unit-only enum (all variants are unit), auto-emit:
1742 // 1. `impl UnitEnumFactor for Self` — provides FACTOR_LEVELS and to_factor_index()
1743 // 2. `impl IntoR for Self` — produces a single-element factor SEXP (cached levels)
1744 // 3. `impl IntoList for Self` — delegates to vec![self].into_list()
1745 //
1746 // The `UnitEnumFactor` impl is consumed by the blanket
1747 // `impl<T: UnitEnumFactor> IntoR for FactorOptionVec<T>` in miniextendr-api,
1748 // which is what `into_data_frame` calls for `as_factor` companion struct columns.
1749 //
1750 // NOTE: `impl IntoR for Vec<Option<Self>>` violates orphan rules (Vec is foreign),
1751 // so we use the `FactorOptionVec<T>` wrapper type (local to miniextendr-api) instead.
1752 //
1753 // These impls allow `as_factor` and `as_list` to work on the inner type when it
1754 // appears as a field of an outer enum or struct DataFrameRow.
1755 let unit_only_factor_impls = {
1756 let all_unit = variant_infos
1757 .iter()
1758 .all(|vi| vi.shape == VariantShape::Unit);
1759 // For unit-only enums, auto-emit three impls:
1760 // 1. `impl UnitEnumFactor for Self` — provides FACTOR_LEVELS and to_factor_index()
1761 // 2. `impl IntoR for Self` — produces a single-element factor SEXP
1762 // 3. `impl IntoList for Self` — delegates to vec![self].into_list()
1763 //
1764 // Non-generic enums: `IntoR` caches the levels SEXP via `OnceLock<SEXP>` (one-time
1765 // `R_PreserveObject`).
1766 //
1767 // Generic enums: Rust does not allow generic statics, so `IntoR` builds the levels
1768 // SEXP on each call using `build_levels_sexp` + manual `Rf_protect`/`Rf_unprotect`.
1769 // This is the same pattern used by `impl<T: UnitEnumFactor> IntoR for FactorOptionVec<T>`
1770 // in `miniextendr-api/src/factor.rs`.
1771 if all_unit {
1772 // Collect variant names and assign 1-based R factor indices (used by both branches).
1773 let variant_idents: Vec<&syn::Ident> =
1774 variant_infos.iter().map(|vi| &vi.name).collect();
1775 let variant_strs: Vec<String> =
1776 variant_infos.iter().map(|vi| vi.name.to_string()).collect();
1777 let variant_strs_lit: Vec<&str> = variant_strs.iter().map(|s| s.as_str()).collect();
1778 let indices: Vec<i32> = (1i32..=(variant_idents.len() as i32)).collect();
1779
1780 if impl_generics.to_token_stream().is_empty() {
1781 // Non-generic: cache levels SEXP permanently via OnceLock (one R_PreserveObject).
1782 quote! {
1783 // impl UnitEnumFactor for Self: provides FACTOR_LEVELS + to_factor_index().
1784 // Used by `impl<T: UnitEnumFactor> IntoR for FactorOptionVec<T>` in miniextendr-api
1785 // to build factor SEXPs from `Vec<Option<Self>>` companion columns.
1786 impl ::miniextendr_api::factor::UnitEnumFactor for #row_name {
1787 const FACTOR_LEVELS: &'static [&'static str] = &[#(#variant_strs_lit),*];
1788 fn to_factor_index(self) -> i32 {
1789 match self {
1790 #(#row_name::#variant_idents => #indices,)*
1791 }
1792 }
1793 fn from_factor_index(idx: i32) -> ::core::option::Option<Self> {
1794 match idx {
1795 #(#indices => ::core::option::Option::Some(#row_name::#variant_idents),)*
1796 _ => ::core::option::Option::None,
1797 }
1798 }
1799 }
1800
1801 // impl IntoR for Self: single-element factor SEXP (cached levels via OnceLock).
1802 // Used when a unit-only enum value is returned directly from a #[miniextendr] fn.
1803 impl ::miniextendr_api::IntoR for #row_name {
1804 type Error = ::std::convert::Infallible;
1805 fn try_into_sexp(self) -> ::std::result::Result<::miniextendr_api::SEXP, Self::Error> {
1806 use ::std::sync::OnceLock;
1807 const LEVELS: &[&str] = &[#(#variant_strs_lit),*];
1808 static LEVELS_CACHE: OnceLock<::miniextendr_api::SEXP> =
1809 OnceLock::new();
1810 let levels = *LEVELS_CACHE.get_or_init(|| {
1811 ::miniextendr_api::factor::build_levels_sexp_cached(LEVELS)
1812 });
1813 let idx: i32 = match self {
1814 #(#row_name::#variant_idents => #indices,)*
1815 };
1816 ::std::result::Result::Ok(
1817 ::miniextendr_api::factor::build_factor(&[idx], levels)
1818 )
1819 }
1820 }
1821
1822 // impl IntoList for Self: for as_list path in outer DataFrameRow.
1823 // Delegates to Vec<Self>: IntoList (blanket impl via IntoR for Self).
1824 impl ::miniextendr_api::list::IntoList for #row_name {
1825 fn into_list(self) -> ::miniextendr_api::list::List {
1826 ::miniextendr_api::list::IntoList::into_list(::std::vec![self])
1827 }
1828 }
1829 }
1830 } else {
1831 // Generic: cannot use generic statics (Rust restriction).
1832 // Build the levels SEXP on each call and protect it across the build_factor
1833 // allocation — same pattern as `FactorOptionVec<T>: IntoR` in
1834 // `miniextendr-api/src/factor.rs`.
1835 quote! {
1836 // impl UnitEnumFactor: associated const is allowed in generic impls.
1837 impl #impl_generics ::miniextendr_api::factor::UnitEnumFactor
1838 for #row_name #ty_generics #where_clause
1839 {
1840 const FACTOR_LEVELS: &'static [&'static str] = &[#(#variant_strs_lit),*];
1841 fn to_factor_index(self) -> i32 {
1842 match self {
1843 #(#row_name::#variant_idents => #indices,)*
1844 }
1845 }
1846 fn from_factor_index(idx: i32) -> ::core::option::Option<Self> {
1847 match idx {
1848 #(#indices => ::core::option::Option::Some(#row_name::#variant_idents),)*
1849 _ => ::core::option::Option::None,
1850 }
1851 }
1852 }
1853
1854 // impl IntoR: build levels SEXP on each call (no generic static allowed).
1855 // build_factor_with_levels handles the PROTECT discipline internally —
1856 // see CLAUDE.md "PROTECT discipline against R-devel GC".
1857 impl #impl_generics ::miniextendr_api::IntoR
1858 for #row_name #ty_generics #where_clause
1859 {
1860 type Error = ::std::convert::Infallible;
1861 fn try_into_sexp(self) -> ::std::result::Result<::miniextendr_api::SEXP, Self::Error> {
1862 const LEVELS: &[&str] = &[#(#variant_strs_lit),*];
1863 let idx: i32 = match self {
1864 #(#row_name::#variant_idents => #indices,)*
1865 };
1866 ::std::result::Result::Ok(
1867 ::miniextendr_api::factor::build_factor_with_levels(&[idx], LEVELS)
1868 )
1869 }
1870 }
1871
1872 // impl IntoList: for as_list path in outer DataFrameRow.
1873 impl #impl_generics ::miniextendr_api::list::IntoList
1874 for #row_name #ty_generics #where_clause
1875 {
1876 fn into_list(self) -> ::miniextendr_api::list::List {
1877 ::miniextendr_api::list::IntoList::into_list(::std::vec![self])
1878 }
1879 }
1880 }
1881 }
1882 } else {
1883 TokenStream::new()
1884 }
1885 };
1886 // endregion
1887
1888 // The enum reader was already computed above as `enum_reader_early`; alias it here
1889 // for the DataFrameRowConvert override logic.
1890 let enum_reader = enum_reader_early;
1891
1892 // region: DataFrameRowConvert on Row — orphan-rule bridge
1893 //
1894 // Same rationale as the struct path: `impl IntoDataFrame for Vec<Row>` is an orphan-rule
1895 // violation in the user crate, so the derive implements the local `DataFrameRowConvert`
1896 // marker on the local enum `Row`, and miniextendr_api's blanket provides the public
1897 // `Vec<Row>: IntoDataFrame`. Tagged enum shapes with reader-capable fields get a
1898 // `rows_from_dataframe` override; other shapes keep the trait default (`None`).
1899 // The build delegates to the companion engine via `ColumnSource::into_dataframe`;
1900 // the parallel path uses the #777 scatter-write builder when one was generated
1901 // for this shape, else the sequential transposition.
1902 let has_par_builder = !from_rows_par_method.is_empty();
1903 let rows_into_dataframe_par_body = if has_par_builder {
1904 quote! {
1905 ::miniextendr_api::convert::ColumnSource::into_dataframe(
1906 <#df_name #ty_generics as ::miniextendr_api::dataframe::ColumnarFrame<
1907 #row_name #ty_generics,
1908 >>::from_rows_par(rows),
1909 )
1910 }
1911 } else {
1912 quote! { Self::rows_into_dataframe(rows) }
1913 };
1914
1915 // rows_from_dataframe / rows_from_dataframe_par overrides (only when reader-capable).
1916 let reader_override = if let Some(ref reader_ts) = enum_reader {
1917 // Check whether the reader uses any Struct fields that would need Clone in par path.
1918 let has_struct_field_any = variant_infos.iter().any(|vi| {
1919 vi.fields
1920 .iter()
1921 .any(|f| matches!(f, EnumResolvedField::Struct(_)))
1922 });
1923 let par_reader_body = if has_struct_field_any {
1924 quote! { Self::try_from_dataframe(__df.as_sexp()) }
1925 } else {
1926 quote! { Self::try_from_dataframe_par(__df.as_sexp()) }
1927 };
1928 let _ = reader_ts; // used below in row_methods
1929 quote! {
1930 fn rows_from_dataframe(
1931 __df: &::miniextendr_api::dataframe::DataFrame,
1932 ) -> ::core::option::Option<::core::result::Result<Vec<Self>, ::miniextendr_api::dataframe::DataFrameError>> {
1933 ::core::option::Option::Some(
1934 <#row_name #ty_generics>::try_from_dataframe(__df.as_sexp())
1935 .map_err(::miniextendr_api::dataframe::DataFrameError::Conversion)
1936 )
1937 }
1938
1939 ::miniextendr_api::__dataframe_row_when_rayon! {
1940 fn rows_from_dataframe_par(
1941 __df: &::miniextendr_api::dataframe::DataFrame,
1942 ) -> ::core::option::Option<::core::result::Result<Vec<Self>, ::miniextendr_api::dataframe::DataFrameError>> {
1943 ::core::option::Option::Some(
1944 #par_reader_body
1945 .map_err(::miniextendr_api::dataframe::DataFrameError::Conversion)
1946 )
1947 }
1948 }
1949 }
1950 } else {
1951 TokenStream::new()
1952 };
1953
1954 let datarow_convert_impl = quote! {
1955 impl #impl_generics ::miniextendr_api::dataframe::DataFrameRowConvert
1956 for #row_name #ty_generics #where_clause
1957 {
1958 fn rows_into_dataframe(
1959 rows: Vec<Self>,
1960 ) -> ::core::result::Result<
1961 ::miniextendr_api::dataframe::DataFrame,
1962 ::miniextendr_api::dataframe::DataFrameError,
1963 > {
1964 ::miniextendr_api::convert::ColumnSource::into_dataframe(
1965 <#row_name #ty_generics>::to_dataframe(rows),
1966 )
1967 }
1968
1969 ::miniextendr_api::__dataframe_row_when_rayon! {
1970 fn rows_into_dataframe_par(
1971 rows: Vec<Self>,
1972 ) -> ::core::result::Result<
1973 ::miniextendr_api::dataframe::DataFrame,
1974 ::miniextendr_api::dataframe::DataFrameError,
1975 > {
1976 #rows_into_dataframe_par_body
1977 }
1978 }
1979
1980 #reader_override
1981 }
1982 };
1983 // endregion
1984
1985 Ok(quote! {
1986 #dataframe_struct
1987 #into_dataframe_impl
1988 #from_vec_impl
1989 #df_methods
1990 #row_methods
1991 #split_method
1992 #marker_impl
1993 #payload_fields_impl
1994 #datarow_convert_impl
1995 #(#struct_assertions)*
1996 #(#payload_collision_assertions)*
1997 #(#sibling_collision_assertions)*
1998 #unit_only_factor_impls
1999 })
2000 // endregion
2001}
2002
2003// region: generate_split_method
2004
2005/// Generate the split representation for an enum `DataFrameRow`: the hidden
2006/// `to_dataframe_split` associated method (body holder) plus the
2007/// `DataFrameRowSplit` bridge impl that surfaces the public
2008/// `rows.into_dataframe_split()` verb (`IntoDataFrameSplit`).
2009///
2010/// For a single-variant enum, returns the data.frame directly.
2011/// For multi-variant enums, returns a named R list of data.frames (one per variant,
2012/// named with snake_case variant names). Each partition data.frame has only that
2013/// variant's columns (non-optional types — no NA fill from other variants).
2014fn generate_split_method(
2015 row_name: &syn::Ident,
2016 variant_infos: &[VariantInfo],
2017 impl_generics: &syn::ImplGenerics<'_>,
2018 ty_generics: &syn::TypeGenerics<'_>,
2019 where_clause: Option<&syn::WhereClause>,
2020) -> TokenStream {
2021 // Per-variant buffer declarations
2022 let mut buf_decls: Vec<TokenStream> = Vec::new();
2023 // Per-variant match arms (push to buffers)
2024 let mut match_arms: Vec<TokenStream> = Vec::new();
2025 // Per-variant data.frame construction
2026 let mut df_constructions: Vec<TokenStream> = Vec::new();
2027 // Names of the constructed data.frame variables (for the outer list)
2028 let mut df_var_names: Vec<syn::Ident> = Vec::new();
2029 // Snake-case string names (for the outer list pairs)
2030 let mut snake_names: Vec<String> = Vec::new();
2031
2032 for vi in variant_infos {
2033 let variant_name = &vi.name;
2034 let snake = naming::to_snake_case(&variant_name.to_string());
2035 snake_names.push(snake.clone());
2036
2037 let df_var = format_ident!("__{}_df", snake);
2038 df_var_names.push(df_var.clone());
2039
2040 // Determine if any field is AutoExpandVec or Struct (both require the dynamic pairs path
2041 // because column names are only known at runtime).
2042 let has_auto = vi.fields.iter().any(|f| {
2043 matches!(
2044 f,
2045 EnumResolvedField::AutoExpandVec(_) | EnumResolvedField::Struct(_)
2046 )
2047 });
2048
2049 match vi.shape {
2050 // region: Unit variant
2051 VariantShape::Unit => {
2052 let count_var = format_ident!("__s_{}_count", snake);
2053 buf_decls.push(quote! {
2054 let mut #count_var: usize = 0usize;
2055 });
2056
2057 match_arms.push(quote! {
2058 #row_name::#variant_name => {
2059 #count_var += 1;
2060 }
2061 });
2062
2063 df_constructions.push(quote! {
2064 let #df_var = ::miniextendr_api::list::List::from_raw_pairs_empty()
2065 .set_class_str(&["data.frame"])
2066 .set_row_names_int(#count_var);
2067 });
2068 }
2069 // endregion
2070
2071 // region: Named or Tuple variants
2072 VariantShape::Named | VariantShape::Tuple => {
2073 // Declare per-field buffers
2074 for erf in &vi.fields {
2075 match erf {
2076 EnumResolvedField::Single(data) => {
2077 let buf = format_ident!("__s_{}_{}", snake, data.col_name);
2078 let ty = &data.ty;
2079 // For needs_into_list fields, ty is already List (the stored type).
2080 buf_decls.push(quote! {
2081 let mut #buf: Vec<#ty> = Vec::new();
2082 });
2083 }
2084 EnumResolvedField::ExpandedFixed(data) => {
2085 for i in 1..=data.len {
2086 let buf = format_ident!("__s_{}_{}_{}", snake, data.base_name, i);
2087 let elem_ty = &data.elem_ty;
2088 buf_decls.push(quote! {
2089 let mut #buf: Vec<#elem_ty> = Vec::new();
2090 });
2091 }
2092 }
2093 EnumResolvedField::ExpandedVec(data) => {
2094 for i in 1..=data.width {
2095 let buf = format_ident!("__s_{}_{}_{}", snake, data.base_name, i);
2096 let elem_ty = &data.elem_ty;
2097 buf_decls.push(quote! {
2098 let mut #buf: Vec<Option<#elem_ty>> = Vec::new();
2099 });
2100 }
2101 }
2102 EnumResolvedField::AutoExpandVec(data) => {
2103 let buf = format_ident!("__s_{}_{}", snake, data.base_name);
2104 let container_ty = &data.container_ty;
2105 buf_decls.push(quote! {
2106 let mut #buf: Vec<#container_ty> = Vec::new();
2107 });
2108 }
2109 EnumResolvedField::Map(data) => {
2110 let keys_buf = format_ident!("__s_{}_{}_keys", snake, data.base_name);
2111 let vals_buf = format_ident!("__s_{}_{}_values", snake, data.base_name);
2112 let key_ty = &data.key_ty;
2113 let val_ty = &data.val_ty;
2114 buf_decls.push(quote! {
2115 let mut #keys_buf: Vec<Vec<#key_ty>> = Vec::new();
2116 let mut #vals_buf: Vec<Vec<#val_ty>> = Vec::new();
2117 });
2118 }
2119 // Struct field: buffer holds Vec<Inner> (no Option — split only sees
2120 // rows of this variant, so every row has the field present).
2121 EnumResolvedField::Struct(data) => {
2122 let buf = format_ident!("__s_{}_{}", snake, data.base_name);
2123 let inner_ty = &data.inner_ty;
2124 buf_decls.push(quote! {
2125 let mut #buf: Vec<#inner_ty> = Vec::new();
2126 });
2127 }
2128 }
2129 }
2130
2131 // Build destructure pattern and push statements
2132 let push_stmts: Vec<TokenStream> = vi
2133 .fields
2134 .iter()
2135 .flat_map(|erf| {
2136 let binding = erf.binding();
2137 match erf {
2138 EnumResolvedField::Single(data) => {
2139 let buf = format_ident!("__s_{}_{}", snake, data.col_name);
2140 vec![quote! { #buf.push(#binding); }]
2141 }
2142 EnumResolvedField::ExpandedFixed(data) => (0..data.len)
2143 .map(|i| {
2144 let buf =
2145 format_ident!("__s_{}_{}_{}", snake, data.base_name, i + 1);
2146 let idx = syn::Index::from(i);
2147 quote! { #buf.push(#binding[#idx]); }
2148 })
2149 .collect(),
2150 EnumResolvedField::ExpandedVec(data) => (0..data.width)
2151 .map(|i| {
2152 let buf =
2153 format_ident!("__s_{}_{}_{}", snake, data.base_name, i + 1);
2154 quote! { #buf.push(#binding.get(#i).cloned()); }
2155 })
2156 .collect(),
2157 EnumResolvedField::AutoExpandVec(data) => {
2158 let buf = format_ident!("__s_{}_{}", snake, data.base_name);
2159 vec![quote! { #buf.push(#binding); }]
2160 }
2161 EnumResolvedField::Map(data) => {
2162 let keys_buf =
2163 format_ident!("__s_{}_{}_keys", snake, data.base_name);
2164 let vals_buf =
2165 format_ident!("__s_{}_{}_values", snake, data.base_name);
2166 // unzip() guarantees pairwise alignment of keys and values.
2167 vec![quote! {
2168 let (__mx_keys, __mx_vals) = #binding.into_iter().unzip::<_, _, Vec<_>, Vec<_>>();
2169 #keys_buf.push(__mx_keys);
2170 #vals_buf.push(__mx_vals);
2171 }]
2172 }
2173 // Struct field: push binding directly (split only sees this variant's rows,
2174 // so every row has the field — no Option needed).
2175 EnumResolvedField::Struct(data) => {
2176 let buf = format_ident!("__s_{}_{}", snake, data.base_name);
2177 vec![quote! { #buf.push(#binding); }]
2178 }
2179 }
2180 })
2181 .collect();
2182
2183 let arm = match vi.shape {
2184 VariantShape::Named => {
2185 let mut field_bindings: Vec<TokenStream> = vi
2186 .fields
2187 .iter()
2188 .map(|erf| {
2189 let rust_name = erf.rust_name();
2190 let binding = erf.binding();
2191 quote! { #rust_name: #binding }
2192 })
2193 .collect();
2194 for skipped in &vi.skipped_fields {
2195 field_bindings.push(quote! { #skipped: _ });
2196 }
2197 quote! {
2198 #row_name::#variant_name { #(#field_bindings),* } => {
2199 #(#push_stmts)*
2200 }
2201 }
2202 }
2203 VariantShape::Tuple => {
2204 let bindings: Vec<TokenStream> = vi
2205 .fields
2206 .iter()
2207 .map(|erf| {
2208 let binding = erf.binding();
2209 quote! { #binding }
2210 })
2211 .collect();
2212 quote! {
2213 #row_name::#variant_name(#(#bindings),*) => {
2214 #(#push_stmts)*
2215 }
2216 }
2217 }
2218 VariantShape::Unit => unreachable!("handled above"),
2219 };
2220 match_arms.push(arm);
2221
2222 // Construct the data.frame for this variant
2223 if has_auto {
2224 // Dynamic path: build Vec<(String, SEXP)>
2225 let pairs_var = format_ident!("__pairs_{}", snake);
2226 let n_var = format_ident!("__n_{}", snake);
2227
2228 // Find the first non-dynamic field for the length expression, or first dynamic.
2229 // "Dynamic" = AutoExpandVec or Struct (both use dynamic pairs path).
2230 let len_expr: TokenStream = {
2231 let first_non_dynamic = vi.fields.iter().find(|f| {
2232 !matches!(
2233 f,
2234 EnumResolvedField::AutoExpandVec(_) | EnumResolvedField::Struct(_)
2235 )
2236 });
2237 if let Some(f) = first_non_dynamic {
2238 match f {
2239 EnumResolvedField::Single(data) => {
2240 let buf = format_ident!("__s_{}_{}", snake, data.col_name);
2241 quote! { #buf.len() }
2242 }
2243 EnumResolvedField::ExpandedFixed(data) => {
2244 let buf = format_ident!(
2245 "__s_{}_{}_{}",
2246 snake,
2247 data.base_name,
2248 1usize
2249 );
2250 quote! { #buf.len() }
2251 }
2252 EnumResolvedField::ExpandedVec(data) => {
2253 let buf = format_ident!(
2254 "__s_{}_{}_{}",
2255 snake,
2256 data.base_name,
2257 1usize
2258 );
2259 quote! { #buf.len() }
2260 }
2261 EnumResolvedField::AutoExpandVec(_)
2262 | EnumResolvedField::Struct(_) => unreachable!(),
2263 EnumResolvedField::Map(data) => {
2264 let keys_buf =
2265 format_ident!("__s_{}_{}_keys", snake, data.base_name);
2266 quote! { #keys_buf.len() }
2267 }
2268 }
2269 } else {
2270 // All fields are dynamic — use the first dynamic buf length
2271 if let Some(first) = vi.fields.first() {
2272 match first {
2273 EnumResolvedField::AutoExpandVec(data) => {
2274 let buf = format_ident!("__s_{}_{}", snake, data.base_name);
2275 quote! { #buf.len() }
2276 }
2277 EnumResolvedField::Struct(data) => {
2278 let buf = format_ident!("__s_{}_{}", snake, data.base_name);
2279 quote! { #buf.len() }
2280 }
2281 _ => quote! { 0usize },
2282 }
2283 } else {
2284 quote! { 0usize }
2285 }
2286 }
2287 };
2288
2289 // Static pair pushes — wrap each `into_sexp()` in
2290 // `__scope.protect_raw` to keep prior column SEXPs rooted
2291 // across subsequent allocations
2292 // (reviews/2026-05-07-gctorture-audit.md).
2293 let static_pushes: Vec<TokenStream> = vi
2294 .fields
2295 .iter()
2296 .flat_map(|erf| match erf {
2297 EnumResolvedField::Single(data) => {
2298 let buf = format_ident!("__s_{}_{}", snake, data.col_name);
2299 let col_str = data.col_name.to_string();
2300 let ty = &data.ty;
2301 if data.needs_into_list {
2302 vec![quote! {
2303 {
2304 let __as_list_col: Vec<::miniextendr_api::list::List> =
2305 #buf.into_iter()
2306 .map(|v: #ty| ::miniextendr_api::list::IntoList::into_list(v))
2307 .collect();
2308 #pairs_var.push((
2309 #col_str.to_string(),
2310 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(__as_list_col)),
2311 ));
2312 }
2313 }]
2314 } else if data.is_factor {
2315 // Factor column: convert Vec<T> → FactorOptionVec<T> (all present).
2316 vec![quote! {
2317 #pairs_var.push((
2318 #col_str.to_string(),
2319 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(
2320 ::miniextendr_api::factor::FactorOptionVec::<#ty>::from(
2321 #buf.into_iter().map(|v| ::std::option::Option::Some(v)).collect::<::std::vec::Vec<_>>()
2322 )
2323 )),
2324 ));
2325 }]
2326 } else {
2327 vec![quote! {
2328 #pairs_var.push((
2329 #col_str.to_string(),
2330 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#buf)),
2331 ));
2332 }]
2333 }
2334 }
2335 EnumResolvedField::ExpandedFixed(data) => (1..=data.len)
2336 .map(|i| {
2337 let buf = format_ident!(
2338 "__s_{}_{}_{}", snake, data.base_name, i
2339 );
2340 let col_str = format!("{}_{}", data.base_name, i);
2341 quote! {
2342 #pairs_var.push((
2343 #col_str.to_string(),
2344 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#buf)),
2345 ));
2346 }
2347 })
2348 .collect(),
2349 EnumResolvedField::ExpandedVec(data) => (1..=data.width)
2350 .map(|i| {
2351 let buf = format_ident!(
2352 "__s_{}_{}_{}", snake, data.base_name, i
2353 );
2354 let col_str = format!("{}_{}", data.base_name, i);
2355 quote! {
2356 #pairs_var.push((
2357 #col_str.to_string(),
2358 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#buf)),
2359 ));
2360 }
2361 })
2362 .collect(),
2363 EnumResolvedField::AutoExpandVec(data) => {
2364 let buf = format_ident!("__s_{}_{}", snake, data.base_name);
2365 let base_str = &data.base_name;
2366 let elem_ty = &data.elem_ty;
2367 vec![quote! {
2368 {
2369 let __auto = #buf;
2370 let __max = __auto.iter().map(|v| v.len()).max().unwrap_or(0);
2371 let mut __auto_cols: Vec<Vec<Option<#elem_ty>>> = (0..__max)
2372 .map(|_| Vec::with_capacity(#n_var))
2373 .collect();
2374 for __row_vec in &__auto {
2375 for (__ai, __acol) in __auto_cols.iter_mut().enumerate() {
2376 __acol.push(__row_vec.get(__ai).cloned());
2377 }
2378 }
2379 for (__ai, __acol) in __auto_cols.into_iter().enumerate() {
2380 #pairs_var.push((
2381 format!("{}_{}", #base_str, __ai + 1),
2382 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(__acol)),
2383 ));
2384 }
2385 }
2386 }]
2387 }
2388 EnumResolvedField::Map(data) => {
2389 let keys_buf =
2390 format_ident!("__s_{}_{}_keys", snake, data.base_name);
2391 let vals_buf =
2392 format_ident!("__s_{}_{}_values", snake, data.base_name);
2393 let keys_str = format!("{}_keys", data.base_name);
2394 let vals_str = format!("{}_values", data.base_name);
2395 vec![
2396 quote! {
2397 #pairs_var.push((
2398 #keys_str.to_string(),
2399 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#keys_buf)),
2400 ));
2401 },
2402 quote! {
2403 #pairs_var.push((
2404 #vals_str.to_string(),
2405 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#vals_buf)),
2406 ));
2407 },
2408 ]
2409 }
2410 // Struct field: call Inner::to_dataframe(buf), extract columns,
2411 // push with prefixed names. In the split path, all rows belong to
2412 // this variant so no scatter is needed.
2413 EnumResolvedField::Struct(data) => {
2414 let buf = format_ident!("__s_{}_{}", snake, data.base_name);
2415 let base_str = &data.base_name;
2416 let inner_ty = &data.inner_ty;
2417 vec![quote! {
2418 {
2419 let __inner_df = <#inner_ty>::to_dataframe(#buf);
2420 let __inner_cols = ::miniextendr_api::convert::ColumnSource::into_named_columns(__inner_df);
2421 for (__inner_col_name, __inner_col_sexp) in __inner_cols {
2422 let __prefixed = format!("{}_{}", #base_str, __inner_col_name);
2423 #pairs_var.push((
2424 __prefixed,
2425 __scope.protect_raw(__inner_col_sexp),
2426 ));
2427 }
2428 }
2429 }]
2430 }
2431 })
2432 .collect();
2433
2434 df_constructions.push(quote! {
2435 let #n_var = #len_expr;
2436 // SAFETY: split-method runs on the R main thread; scope
2437 // unprotects after each variant data.frame is built.
2438 let #df_var = unsafe {
2439 let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
2440 let mut #pairs_var: Vec<(String, ::miniextendr_api::SEXP)> = Vec::new();
2441 #(#static_pushes)*
2442 ::miniextendr_api::list::List::from_raw_pairs(#pairs_var)
2443 .set_class_str(&["data.frame"])
2444 .set_row_names_int(#n_var)
2445 };
2446 });
2447 } else {
2448 // Static path: vec![...] of (&str, SEXP) pairs
2449 let n_var = format_ident!("__n_{}", snake);
2450
2451 // Length expression: first field's buffer length
2452 let len_expr: TokenStream = if let Some(erf) = vi.fields.first() {
2453 match erf {
2454 EnumResolvedField::Single(data) => {
2455 let buf = format_ident!("__s_{}_{}", snake, data.col_name);
2456 quote! { #buf.len() }
2457 }
2458 EnumResolvedField::ExpandedFixed(data) => {
2459 let buf =
2460 format_ident!("__s_{}_{}_{}", snake, data.base_name, 1usize);
2461 quote! { #buf.len() }
2462 }
2463 EnumResolvedField::ExpandedVec(data) => {
2464 let buf =
2465 format_ident!("__s_{}_{}_{}", snake, data.base_name, 1usize);
2466 quote! { #buf.len() }
2467 }
2468 // AutoExpandVec and Struct both trigger has_auto = true, so these
2469 // branches are unreachable in the non-auto static path.
2470 EnumResolvedField::AutoExpandVec(_) | EnumResolvedField::Struct(_) => {
2471 unreachable!()
2472 }
2473 EnumResolvedField::Map(data) => {
2474 let keys_buf =
2475 format_ident!("__s_{}_{}_keys", snake, data.base_name);
2476 quote! { #keys_buf.len() }
2477 }
2478 }
2479 } else {
2480 // No fields (unexpected for Named/Tuple, but handle it)
2481 quote! { 0usize }
2482 };
2483
2484 // Collect pairs — each `into_sexp()` is rooted via
2485 // `__scope.protect_raw` so prior columns survive the
2486 // next column's allocation
2487 // (reviews/2026-05-07-gctorture-audit.md).
2488 let pairs: Vec<TokenStream> = vi
2489 .fields
2490 .iter()
2491 .flat_map(|erf| match erf {
2492 EnumResolvedField::Single(data) => {
2493 let buf = format_ident!("__s_{}_{}", snake, data.col_name);
2494 let col_str = data.col_name.to_string();
2495 let ty = &data.ty;
2496 if data.needs_into_list {
2497 // Convert Vec<T> → Vec<List> → SEXP at split time.
2498 vec![quote! {
2499 (#col_str, {
2500 let __as_list_col: Vec<::miniextendr_api::list::List> =
2501 #buf.into_iter()
2502 .map(|v: #ty| ::miniextendr_api::list::IntoList::into_list(v))
2503 .collect();
2504 __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(__as_list_col))
2505 })
2506 }]
2507 } else if data.is_factor {
2508 // Factor: convert Vec<T> → FactorOptionVec<T> (all present).
2509 vec![quote! {
2510 (#col_str, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(
2511 ::miniextendr_api::factor::FactorOptionVec::<#ty>::from(
2512 #buf.into_iter().map(|v| ::std::option::Option::Some(v)).collect::<::std::vec::Vec<_>>()
2513 )
2514 )))
2515 }]
2516 } else {
2517 vec![quote! {
2518 (#col_str, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#buf)))
2519 }]
2520 }
2521 }
2522 EnumResolvedField::ExpandedFixed(data) => (1..=data.len)
2523 .map(|i| {
2524 let buf =
2525 format_ident!("__s_{}_{}_{}", snake, data.base_name, i);
2526 let col_str = format!("{}_{}", data.base_name, i);
2527 quote! {
2528 (#col_str, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#buf)))
2529 }
2530 })
2531 .collect(),
2532 EnumResolvedField::ExpandedVec(data) => (1..=data.width)
2533 .map(|i| {
2534 let buf =
2535 format_ident!("__s_{}_{}_{}", snake, data.base_name, i);
2536 let col_str = format!("{}_{}", data.base_name, i);
2537 quote! {
2538 (#col_str, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#buf)))
2539 }
2540 })
2541 .collect(),
2542 // AutoExpandVec and Struct both trigger has_auto = true.
2543 EnumResolvedField::AutoExpandVec(_) | EnumResolvedField::Struct(_) => unreachable!(),
2544 EnumResolvedField::Map(data) => {
2545 let keys_buf =
2546 format_ident!("__s_{}_{}_keys", snake, data.base_name);
2547 let vals_buf =
2548 format_ident!("__s_{}_{}_values", snake, data.base_name);
2549 let keys_str = format!("{}_keys", data.base_name);
2550 let vals_str = format!("{}_values", data.base_name);
2551 vec![
2552 quote! {
2553 (#keys_str, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#keys_buf)))
2554 },
2555 quote! {
2556 (#vals_str, __scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#vals_buf)))
2557 },
2558 ]
2559 }
2560 })
2561 .collect();
2562
2563 df_constructions.push(quote! {
2564 let #n_var = #len_expr;
2565 // SAFETY: see has_auto branch.
2566 let #df_var = unsafe {
2567 let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
2568 ::miniextendr_api::list::List::from_raw_pairs(vec![
2569 #(#pairs),*
2570 ])
2571 .set_class_str(&["data.frame"])
2572 .set_row_names_int(#n_var)
2573 };
2574 });
2575 }
2576 } // endregion
2577 }
2578 }
2579
2580 // Build the method body
2581 let body = if variant_infos.len() == 1 {
2582 // Single variant: return the data.frame directly
2583 let df_var = &df_var_names[0];
2584 quote! {
2585 #(#buf_decls)*
2586 for __row in rows {
2587 match __row {
2588 #(#match_arms)*
2589 }
2590 }
2591 #(#df_constructions)*
2592 #df_var
2593 }
2594 } else {
2595 // Multiple variants: return named list of data.frames.
2596 // Each per-variant data.frame's `into_sexp()` is rooted via
2597 // `__outer_scope.protect_raw` so prior variant data.frames survive
2598 // the next variant's allocation
2599 // (reviews/2026-05-07-gctorture-audit.md).
2600 let outer_pairs: Vec<TokenStream> = snake_names
2601 .iter()
2602 .zip(df_var_names.iter())
2603 .map(|(name, var)| {
2604 quote! { (#name, __outer_scope.protect_raw(::miniextendr_api::IntoR::into_sexp(#var))) }
2605 })
2606 .collect();
2607
2608 quote! {
2609 #(#buf_decls)*
2610 for __row in rows {
2611 match __row {
2612 #(#match_arms)*
2613 }
2614 }
2615 #(#df_constructions)*
2616 // SAFETY: split-method runs on the R main thread.
2617 unsafe {
2618 let __outer_scope = ::miniextendr_api::gc_protect::ProtectScope::new();
2619 ::miniextendr_api::list::List::from_raw_pairs(vec![
2620 #(#outer_pairs),*
2621 ])
2622 }
2623 }
2624 };
2625
2626 quote! {
2627 impl #impl_generics #row_name #ty_generics #where_clause {
2628 // The documented split verb is `IntoDataFrameSplit::into_dataframe_split`
2629 // (via the `DataFrameRowSplit` bridge below); this inherent holds the
2630 // body and stays `pub` off the documented surface, mirroring the hidden
2631 // `to_dataframe(rows)` companion helper.
2632 #[doc(hidden)]
2633 pub fn to_dataframe_split(rows: Vec<Self>) -> ::miniextendr_api::list::List {
2634 #body
2635 }
2636 }
2637
2638 // Orphan-rule bridge: `impl IntoDataFrameSplit for Vec<Row>` is illegal here
2639 // (`Row` is covered by `Vec<_>`), so implement the local-type bridge trait and
2640 // let `miniextendr_api`'s blanket impl surface `rows.into_dataframe_split()`.
2641 impl #impl_generics ::miniextendr_api::dataframe::DataFrameRowSplit
2642 for #row_name #ty_generics #where_clause
2643 {
2644 fn rows_into_dataframe_split(
2645 rows: ::std::vec::Vec<Self>,
2646 ) -> ::miniextendr_api::list::List {
2647 Self::to_dataframe_split(rows)
2648 }
2649 }
2650 }
2651}
2652// endregion
2653
2654// region: enum reader (#807)
2655
2656/// Check whether an enum field is reader-capable.
2657///
2658/// Mirrors `field_reader_capable` from `dataframe_derive.rs` but for `EnumResolvedField`.
2659/// Key differences vs the struct path:
2660/// - `Single` enum fields are always `Vec<Option<ty>>` (even non-factor) because the
2661/// writer wraps every cell in `Option`; so we only need `is_reader_scalar_ty`
2662/// (which accepts both bare scalars and `Option<scalar>`).
2663/// - `is_factor` Single fields are reader-capable iff the inner type satisfies
2664/// `UnitEnumFactor` — guaranteed by the derive for unit-only enums.
2665/// - `Map` fields are reader-capable iff both `K` and `V` are bare scalar element
2666/// types: the reader regroups the `<base>_keys` / `<base>_values` list-columns
2667/// (each row a `Vec<K>` / `Vec<V>`) back into the map via `Vec<elem>: TryFromSexp`.
2668/// `Option`-wrapped key/value element types are excluded (the writer emits them,
2669/// but the reader path is restricted to the round-trippable bare-scalar set).
2670fn enum_field_reader_capable(erf: &EnumResolvedField) -> bool {
2671 match erf {
2672 EnumResolvedField::Single(data) => {
2673 !data.needs_into_list && (data.is_factor || is_reader_scalar_ty(&data.ty))
2674 }
2675 EnumResolvedField::ExpandedFixed(data) => is_reader_scalar_ty(&data.elem_ty),
2676 EnumResolvedField::ExpandedVec(data) => is_bare_reader_scalar_ty(&data.elem_ty),
2677 EnumResolvedField::AutoExpandVec(data) => is_bare_reader_scalar_ty(&data.elem_ty),
2678 EnumResolvedField::Map(data) => {
2679 is_bare_reader_scalar_ty(&data.key_ty) && is_bare_reader_scalar_ty(&data.val_ty)
2680 }
2681 EnumResolvedField::Struct(_) => true, // routes through inner DataFrameRowConvert
2682 }
2683}
2684
2685/// Build the `try_from_dataframe` / `try_from_dataframe_par` methods for a tagged enum.
2686///
2687/// Returns `None` if the enum is not reader-capable (tagless, has skipped fields,
2688/// has `conflicts = "string"`, or has a field type that can't be read back from R).
2689/// When `None`, the enum keeps the `DataFrameRowConvert` trait default (`rows_from_dataframe
2690/// → None`), which surfaces as a clear `DataFrameError::Conversion` at runtime.
2691#[allow(clippy::too_many_arguments)]
2692fn build_enum_reader(
2693 row_name: &syn::Ident,
2694 variant_infos: &[VariantInfo],
2695 columns: &[ResolvedColumn],
2696 attrs: &DataFrameAttrs,
2697 _impl_generics: &syn::ImplGenerics<'_>,
2698 _ty_generics: &syn::TypeGenerics<'_>,
2699 _where_clause: Option<&syn::WhereClause>,
2700) -> Option<TokenStream> {
2701 // Gate 1: must have a tag column.
2702 let tag_col_name = attrs.tag.as_deref()?;
2703
2704 // Gate 2: no skipped fields.
2705 if variant_infos.iter().any(|vi| !vi.skipped_fields.is_empty()) {
2706 return None;
2707 }
2708
2709 // Gate 3: no string coercion.
2710 if attrs.conflicts.is_some() {
2711 return None;
2712 }
2713
2714 // Gate 4: every field across all variants must be reader-capable.
2715 if !variant_infos
2716 .iter()
2717 .all(|vi| vi.fields.iter().all(enum_field_reader_capable))
2718 {
2719 return None;
2720 }
2721
2722 // Determine if any variant has a Struct field (affects _par strategy).
2723 let has_struct_field = variant_infos.iter().any(|vi| {
2724 vi.fields
2725 .iter()
2726 .any(|f| matches!(f, EnumResolvedField::Struct(_)))
2727 });
2728
2729 // Auto-expand columns are discovered at runtime in the generated code — no prelude needed.
2730
2731 // region: column extraction prelude (R-thread, all SEXP access up front)
2732 // Pull each schema column as Vec<Option<elem>> (every enum column is Option-wrapped
2733 // because absent variants push None).
2734 let mut extracts: Vec<TokenStream> = Vec::new();
2735
2736 // Tag column: pull as Vec<String>.
2737 let tag_var = format_ident!("__tag");
2738 extracts.push(quote! {
2739 let #tag_var: Vec<::std::string::String> = {
2740 let __tag_sexp = __view.column_raw(#tag_col_name).ok_or_else(|| {
2741 ::std::format!("tag column `{}` is missing from the data.frame", #tag_col_name)
2742 })?;
2743 <Vec<::std::string::String> as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__tag_sexp)
2744 .map_err(|e| ::std::format!(
2745 "tag column `{}` could not be read as strings: {}",
2746 #tag_col_name, e
2747 ))?
2748 };
2749 if #tag_var.len() != __nrow {
2750 return ::core::result::Result::Err(::std::format!(
2751 "tag column `{}` has length {} but data.frame has {} rows",
2752 #tag_col_name, #tag_var.len(), __nrow
2753 ));
2754 }
2755 });
2756
2757 // For each static schema column, pull as Vec<Option<ty>>.
2758 // Collect which column names are Struct fields to handle densify separately.
2759 let mut struct_col_names: std::collections::HashSet<String> = std::collections::HashSet::new();
2760 for vi in variant_infos {
2761 for erf in &vi.fields {
2762 if let EnumResolvedField::Struct(data) = erf {
2763 struct_col_names.insert(data.base_name.clone());
2764 }
2765 }
2766 }
2767
2768 // Collect Map column names (`<base>_keys` / `<base>_values`). These are registered
2769 // in `columns` as `Vec<K>` / `Vec<V>` but have no `Vec<Option<Vec<_>>>: TryFromSexp`
2770 // impl, so the generic loop below skips them; bespoke list-column extraction follows.
2771 let mut map_col_names: std::collections::HashSet<String> = std::collections::HashSet::new();
2772 for vi in variant_infos {
2773 for erf in &vi.fields {
2774 if let EnumResolvedField::Map(data) = erf {
2775 map_col_names.insert(format!("{}_keys", data.base_name));
2776 map_col_names.insert(format!("{}_values", data.base_name));
2777 }
2778 }
2779 }
2780
2781 for col in columns {
2782 let col_name_str = col.col_name.to_string();
2783 let col_var = format_ident!("__col_{}", col.col_name);
2784 let ty = &col.ty;
2785
2786 // Skip Struct columns — they are handled separately via sub-frame densify.
2787 if struct_col_names.contains(&col_name_str) {
2788 continue;
2789 }
2790 // Skip Map columns — handled separately via the list-column regroup below.
2791 if map_col_names.contains(&col_name_str) {
2792 continue;
2793 }
2794
2795 if col.is_factor {
2796 // as_factor column: use unit_factor_option_vec_from_sexp.
2797 extracts.push(quote! {
2798 let #col_var: Vec<::core::option::Option<#ty>> = {
2799 let __col_sexp = __view.column_raw(#col_name_str).ok_or_else(|| {
2800 ::std::format!("column `{}` is missing from the data.frame", #col_name_str)
2801 })?;
2802 ::miniextendr_api::factor::unit_factor_option_vec_from_sexp::<#ty>(__col_sexp)
2803 .map_err(|e| ::std::format!(
2804 "factor column `{}` could not be read: {}",
2805 #col_name_str, e
2806 ))?
2807 };
2808 if #col_var.len() != __nrow {
2809 return ::core::result::Result::Err(::std::format!(
2810 "column `{}` has length {} but data.frame has {} rows",
2811 #col_name_str, #col_var.len(), __nrow
2812 ));
2813 }
2814 });
2815 } else {
2816 // Regular column: pull as Vec<Option<ty>> via TryFromSexp.
2817 let opt_ty: syn::Type = syn::parse_quote!(::core::option::Option<#ty>);
2818 extracts.push(quote! {
2819 let #col_var: Vec<#opt_ty> = {
2820 let __col_sexp = __view.column_raw(#col_name_str).ok_or_else(|| {
2821 ::std::format!("column `{}` is missing from the data.frame", #col_name_str)
2822 })?;
2823 <Vec<#opt_ty> as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__col_sexp)
2824 .map_err(|e| ::std::format!(
2825 "column `{}` could not be converted to the expected type: {}",
2826 #col_name_str, e
2827 ))?
2828 };
2829 if #col_var.len() != __nrow {
2830 return ::core::result::Result::Err(::std::format!(
2831 "column `{}` has length {} but data.frame has {} rows",
2832 #col_name_str, #col_var.len(), __nrow
2833 ));
2834 }
2835 });
2836 }
2837 }
2838
2839 // For each auto-expand field, discover columns at runtime.
2840 // We collect the set of unique auto-expand base names across all variants.
2841 let mut auto_expand_base_names: Vec<(String, syn::Type)> = Vec::new();
2842 let mut seen_auto: std::collections::HashSet<String> = std::collections::HashSet::new();
2843 for vi in variant_infos {
2844 for erf in &vi.fields {
2845 if let EnumResolvedField::AutoExpandVec(data) = erf
2846 && seen_auto.insert(data.base_name.clone())
2847 {
2848 auto_expand_base_names.push((data.base_name.clone(), data.elem_ty.clone()));
2849 }
2850 }
2851 }
2852
2853 for (base_name, elem_ty) in &auto_expand_base_names {
2854 let cols_var = format_ident!("__aev_{}", base_name.replace('-', "_"));
2855 let opt_elem_ty: syn::Type = syn::parse_quote!(::core::option::Option<#elem_ty>);
2856 extracts.push(quote! {
2857 let #cols_var: Vec<Vec<#opt_elem_ty>> = {
2858 let mut __cols: Vec<Vec<#opt_elem_ty>> = ::std::vec::Vec::new();
2859 let mut __k: usize = 1;
2860 loop {
2861 let __cn = ::std::format!("{}_{}", #base_name, __k);
2862 match __view.column_raw(&__cn) {
2863 ::core::option::Option::Some(__s) => {
2864 let __c: Vec<#opt_elem_ty> =
2865 <Vec<#opt_elem_ty> as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__s)
2866 .map_err(|e| ::std::format!(
2867 "column `{}` could not be converted: {}",
2868 __cn, e
2869 ))?;
2870 if __c.len() != __nrow {
2871 return ::core::result::Result::Err(::std::format!(
2872 "column `{}` has length {} but data.frame has {} rows",
2873 __cn, __c.len(), __nrow
2874 ));
2875 }
2876 __cols.push(__c);
2877 __k += 1;
2878 }
2879 ::core::option::Option::None => break,
2880 }
2881 }
2882 __cols
2883 };
2884 });
2885 }
2886
2887 // For each Struct field, build a densified sub-Vec<Option<Inner>>.
2888 // Approach: for each struct field base_name, collect the present_in indices from
2889 // the variant_infos, build a presence mask, select+strip_prefix the sub-frame,
2890 // densify it via select_rows, recurse via DataFrameRowConvert, scatter to
2891 // Vec<Option<Inner>> of length __nrow.
2892 let mut seen_struct: std::collections::HashSet<String> = std::collections::HashSet::new();
2893 for vi in variant_infos {
2894 for erf in &vi.fields {
2895 if let EnumResolvedField::Struct(data) = erf
2896 && seen_struct.insert(data.base_name.clone())
2897 {
2898 let inner_ty = &data.inner_ty;
2899 let base = &data.base_name;
2900 let prefix_lit = format!("{}_", data.base_name);
2901 let vec_var = format_ident!("__sf_{}", data.base_name.replace('-', "_"));
2902
2903 // Collect the variant names that contribute this field (for the mask).
2904 let contributing_variant_names: Vec<String> = variant_infos
2905 .iter()
2906 .filter(|vi2| {
2907 vi2.fields.iter().any(
2908 |f| matches!(f, EnumResolvedField::Struct(d) if d.base_name == *base),
2909 )
2910 })
2911 .map(|vi2| vi2.name.to_string())
2912 .collect();
2913
2914 extracts.push(quote! {
2915 let #vec_var: Vec<::core::option::Option<#inner_ty>> = {
2916 // Build presence mask: row i is present iff its tag is one of the
2917 // variants that contribute this Struct field.
2918 let __contributing: &[&str] = &[#(#contributing_variant_names),*];
2919 let __present_indices: Vec<usize> = (0..__nrow)
2920 .filter(|&__i| __contributing.contains(&__tag[__i].as_str()))
2921 .collect();
2922
2923 let __inner_result: Vec<#inner_ty> = if __present_indices.is_empty() {
2924 ::std::vec::Vec::new()
2925 } else {
2926 // Select the prefixed columns into a sub-frame, strip prefix,
2927 // densify to present rows only, then recurse via inner reader.
2928 let __prefix: &str = #prefix_lit;
2929 let __names = __view.names();
2930 let __sel: Vec<&str> = __names
2931 .iter()
2932 .filter(|__n| __n.starts_with(__prefix))
2933 .map(|__n| __n.as_str())
2934 .collect();
2935 if __sel.is_empty() {
2936 return ::core::result::Result::Err(::std::format!(
2937 "struct column `{}`: no columns with prefix `{}` found",
2938 #base, __prefix
2939 ));
2940 }
2941 // Each edit returns an owned, GC-rooted `BuiltDataFrame`
2942 // (#1247): `select` roots the sub-frame, `strip_prefix`
2943 // (inherent forward) carries the handle through its
2944 // CHARSXP allocations, and `select_rows` roots the
2945 // densified frame before the borrowed handle's root
2946 // releases — every intermediate frame is rooted across
2947 // the next step's allocations and the recursive read.
2948 let __sub_dense = __view
2949 .select(&__sel)
2950 .strip_prefix(__prefix)
2951 .select_rows(&__present_indices);
2952 let __out = match <#inner_ty as ::miniextendr_api::dataframe::DataFrameRowConvert>::rows_from_dataframe(&__sub_dense) {
2953 ::core::option::Option::Some(::core::result::Result::Ok(__v)) => __v,
2954 ::core::option::Option::Some(::core::result::Result::Err(__e)) => {
2955 return ::core::result::Result::Err(::std::format!(
2956 "struct column `{}`: {}", #base, __e
2957 ));
2958 }
2959 ::core::option::Option::None => {
2960 return ::core::result::Result::Err(::std::format!(
2961 "struct column `{}`: nested type has no data.frame reader", #base
2962 ));
2963 }
2964 };
2965 __out
2966 };
2967
2968 // Scatter dense Vec<Inner> back to Vec<Option<Inner>> of length __nrow.
2969 let mut __result: Vec<::core::option::Option<#inner_ty>> =
2970 (0..__nrow).map(|_| ::core::option::Option::None).collect();
2971 let mut __dense_iter = __inner_result.into_iter();
2972 for &__i in &__present_indices {
2973 __result[__i] = ::core::option::Option::Some(
2974 __dense_iter.next().expect("dense iter must match present count")
2975 );
2976 }
2977 __result
2978 };
2979 });
2980 }
2981 }
2982 }
2983
2984 // For each Map field, read the parallel `<base>_keys` / `<base>_values`
2985 // list-columns back into `Vec<Option<Vec<K>>>` / `Vec<Option<Vec<V>>>`. The
2986 // writer emits these via `Vec<Option<Vec<_>>>: IntoR` (a VECSXP where absent-
2987 // variant rows are NULL and present rows are typed vectors), so the reader
2988 // walks the VECSXP: NULL → `None`, typed vector → `Some(Vec<elem>)`. The
2989 // per-row dispatch zips `keys[i]` and `values[i]` back into the map type.
2990 let emit_map_col_extract = |col_var: &syn::Ident, col_name: &str, elem_ty: &syn::Type| {
2991 quote! {
2992 let #col_var: Vec<::core::option::Option<Vec<#elem_ty>>> = {
2993 let __col_sexp = __view.column_raw(#col_name).ok_or_else(|| {
2994 ::std::format!("column `{}` is missing from the data.frame", #col_name)
2995 })?;
2996 // VECSXP list-column: NULL → None, typed vector → Some(Vec<elem>).
2997 // `SexpExt::is_list` via UFCS (avoids the `List::is_list` pair-list bug).
2998 if <::miniextendr_api::SEXP as ::miniextendr_api::SexpExt>::is_list(&__col_sexp) {
2999 let __list = unsafe {
3000 ::miniextendr_api::list::List::from_raw(__col_sexp)
3001 };
3002 let __len = __list.len();
3003 let mut __v: Vec<::core::option::Option<Vec<#elem_ty>>> =
3004 ::std::vec::Vec::with_capacity(__len as usize);
3005 for __j in 0..__len {
3006 // in-bounds by construction (0..len)
3007 let __elt = __list.get(__j).unwrap();
3008 if __elt == ::miniextendr_api::SEXP::nil() {
3009 __v.push(::core::option::Option::None);
3010 } else {
3011 let __inner: Vec<#elem_ty> =
3012 <Vec<#elem_ty> as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__elt)
3013 .map_err(|e| ::std::format!(
3014 "column `{}` element {} could not be converted to the expected type: {}",
3015 #col_name, __j, e
3016 ))?;
3017 __v.push(::core::option::Option::Some(__inner));
3018 }
3019 }
3020 __v
3021 } else {
3022 // Non-list column → no map present in any row.
3023 (0..__nrow).map(|_| ::core::option::Option::None).collect()
3024 }
3025 };
3026 if #col_var.len() != __nrow {
3027 return ::core::result::Result::Err(::std::format!(
3028 "column `{}` has length {} but data.frame has {} rows",
3029 #col_name, #col_var.len(), __nrow
3030 ));
3031 }
3032 }
3033 };
3034 let mut seen_map: std::collections::HashSet<String> = std::collections::HashSet::new();
3035 for vi in variant_infos {
3036 for erf in &vi.fields {
3037 if let EnumResolvedField::Map(data) = erf
3038 && seen_map.insert(data.base_name.clone())
3039 {
3040 let keys_name = format!("{}_keys", data.base_name);
3041 let vals_name = format!("{}_values", data.base_name);
3042 let keys_var = format_ident!("__mapcol_{}_keys", data.base_name.replace('-', "_"));
3043 let vals_var =
3044 format_ident!("__mapcol_{}_values", data.base_name.replace('-', "_"));
3045 extracts.push(emit_map_col_extract(&keys_var, &keys_name, &data.key_ty));
3046 extracts.push(emit_map_col_extract(&vals_var, &vals_name, &data.val_ty));
3047 }
3048 }
3049 }
3050 // endregion
3051
3052 // region: per-row dispatch match arms
3053 let mut match_arms: Vec<TokenStream> = Vec::new();
3054 // Parallel variant: arms return Ok(row) instead of pushing to a Vec.
3055 let mut par_match_arms: Vec<TokenStream> = Vec::new();
3056
3057 for (variant_idx, vi) in variant_infos.iter().enumerate() {
3058 let variant_name_str = vi.name.to_string();
3059 let variant_ident = &vi.name;
3060
3061 // Build the field expressions for this variant.
3062 let mut field_exprs: Vec<TokenStream> = Vec::new();
3063
3064 for erf in &vi.fields {
3065 let rust_name = erf.rust_name();
3066
3067 let expr = match erf {
3068 EnumResolvedField::Single(data) => {
3069 let col_var = format_ident!("__col_{}", data.col_name);
3070 let col_name_str = data.col_name.to_string();
3071 if data.is_factor {
3072 quote! {
3073 #rust_name: #col_var[__i].clone().ok_or_else(|| ::std::format!(
3074 "variant `{}` row {}: factor column `{}` is NA but field is required",
3075 #variant_name_str, __i, #col_name_str
3076 ))?
3077 }
3078 } else {
3079 quote! {
3080 #rust_name: #col_var[__i].clone().ok_or_else(|| ::std::format!(
3081 "variant `{}` row {}: column `{}` is NA but field is required",
3082 #variant_name_str, __i, #col_name_str
3083 ))?
3084 }
3085 }
3086 }
3087 EnumResolvedField::ExpandedFixed(data) => {
3088 let elem_ty = &data.elem_ty;
3089 let len = data.len;
3090 let slots: Vec<TokenStream> = (1..=data.len)
3091 .map(|k| {
3092 let col_var = format_ident!("__col_{}_{}", data.base_name, k);
3093 let col_name_str = format!("{}_{}", data.base_name, k);
3094 quote! {
3095 #col_var[__i].clone().ok_or_else(|| ::std::format!(
3096 "variant `{}` row {}: column `{}` is NA but field is required",
3097 #variant_name_str, __i, #col_name_str
3098 ))?
3099 }
3100 })
3101 .collect();
3102 quote! {
3103 #rust_name: {
3104 let __arr: [#elem_ty; #len] = [ #(#slots),* ];
3105 __arr
3106 }
3107 }
3108 }
3109 EnumResolvedField::ExpandedVec(data) => {
3110 let elem_ty = &data.elem_ty;
3111 let slots: Vec<TokenStream> = (1..=data.width)
3112 .map(|k| {
3113 let col_var = format_ident!("__col_{}_{}", data.base_name, k);
3114 quote! { #col_var[__i].clone() }
3115 })
3116 .collect();
3117 quote! {
3118 #rust_name: [ #(#slots),* ]
3119 .into_iter().flatten().collect::<Vec<#elem_ty>>().into()
3120 }
3121 }
3122 EnumResolvedField::AutoExpandVec(data) => {
3123 let elem_ty = &data.elem_ty;
3124 let cols_var = format_ident!("__aev_{}", data.base_name.replace('-', "_"));
3125 quote! {
3126 #rust_name: #cols_var
3127 .iter()
3128 .filter_map(|__c| __c[__i].clone())
3129 .collect::<Vec<#elem_ty>>()
3130 .into()
3131 }
3132 }
3133 EnumResolvedField::Map(data) => {
3134 let keys_var =
3135 format_ident!("__mapcol_{}_keys", data.base_name.replace('-', "_"));
3136 let vals_var =
3137 format_ident!("__mapcol_{}_values", data.base_name.replace('-', "_"));
3138 let base = &data.base_name;
3139 let map_ty = &data.map_ty;
3140 quote! {
3141 #rust_name: {
3142 let __keys = #keys_var[__i].clone().ok_or_else(|| ::std::format!(
3143 "variant `{}` row {}: map column `{}` is NA but field is required",
3144 #variant_name_str, __i, #base
3145 ))?;
3146 let __vals = #vals_var[__i].clone().ok_or_else(|| ::std::format!(
3147 "variant `{}` row {}: map column `{}` is NA but field is required",
3148 #variant_name_str, __i, #base
3149 ))?;
3150 if __keys.len() != __vals.len() {
3151 return ::core::result::Result::Err(::std::format!(
3152 "variant `{}` row {}: map column `{}` has {} keys but {} values",
3153 #variant_name_str, __i, #base, __keys.len(), __vals.len()
3154 ));
3155 }
3156 __keys.into_iter().zip(__vals).collect::<#map_ty>()
3157 }
3158 }
3159 }
3160 EnumResolvedField::Struct(data) => {
3161 let vec_var = format_ident!("__sf_{}", data.base_name.replace('-', "_"));
3162 let base = &data.base_name;
3163 quote! {
3164 #rust_name: #vec_var[__i].clone().ok_or_else(|| ::std::format!(
3165 "variant `{}` row {}: struct field `{}` is absent for this variant",
3166 #variant_name_str, __i, #base
3167 ))?
3168 }
3169 }
3170 };
3171 field_exprs.push(expr);
3172 }
3173
3174 // Build the match arm for this variant.
3175 let arm_body = match vi.shape {
3176 VariantShape::Named => {
3177 if field_exprs.is_empty() {
3178 quote! { #row_name::#variant_ident {} }
3179 } else {
3180 quote! { #row_name::#variant_ident { #(#field_exprs),* } }
3181 }
3182 }
3183 VariantShape::Tuple => {
3184 // For tuple variants we need positional args, not `rust_name: expr`.
3185 // Rebuild expressions without the `rust_name:` prefix.
3186 let positional_exprs: Vec<TokenStream> = vi.fields.iter().map(|erf| {
3187 match erf {
3188 EnumResolvedField::Single(data) => {
3189 let col_var = format_ident!("__col_{}", data.col_name);
3190 let col_name_str = data.col_name.to_string();
3191 if data.is_factor {
3192 quote! {
3193 #col_var[__i].clone().ok_or_else(|| ::std::format!(
3194 "variant `{}` row {}: factor column `{}` is NA but field is required",
3195 #variant_name_str, __i, #col_name_str
3196 ))?
3197 }
3198 } else {
3199 quote! {
3200 #col_var[__i].clone().ok_or_else(|| ::std::format!(
3201 "variant `{}` row {}: column `{}` is NA but field is required",
3202 #variant_name_str, __i, #col_name_str
3203 ))?
3204 }
3205 }
3206 }
3207 EnumResolvedField::ExpandedFixed(data) => {
3208 let elem_ty = &data.elem_ty;
3209 let len = data.len;
3210 let slots: Vec<TokenStream> = (1..=data.len)
3211 .map(|k| {
3212 let col_var = format_ident!("__col_{}_{}", data.base_name, k);
3213 let col_name_str = format!("{}_{}", data.base_name, k);
3214 quote! {
3215 #col_var[__i].clone().ok_or_else(|| ::std::format!(
3216 "variant `{}` row {}: column `{}` is NA but field is required",
3217 #variant_name_str, __i, #col_name_str
3218 ))?
3219 }
3220 })
3221 .collect();
3222 quote! { { let __arr: [#elem_ty; #len] = [ #(#slots),* ]; __arr } }
3223 }
3224 EnumResolvedField::ExpandedVec(data) => {
3225 let elem_ty = &data.elem_ty;
3226 let slots: Vec<TokenStream> = (1..=data.width)
3227 .map(|k| {
3228 let col_var = format_ident!("__col_{}_{}", data.base_name, k);
3229 quote! { #col_var[__i].clone() }
3230 })
3231 .collect();
3232 quote! { [ #(#slots),* ].into_iter().flatten().collect::<Vec<#elem_ty>>().into() }
3233 }
3234 EnumResolvedField::AutoExpandVec(data) => {
3235 let elem_ty = &data.elem_ty;
3236 let cols_var = format_ident!("__aev_{}", data.base_name.replace('-', "_"));
3237 quote! {
3238 #cols_var.iter().filter_map(|__c| __c[__i].clone()).collect::<Vec<#elem_ty>>().into()
3239 }
3240 }
3241 EnumResolvedField::Map(data) => {
3242 let keys_var =
3243 format_ident!("__mapcol_{}_keys", data.base_name.replace('-', "_"));
3244 let vals_var = format_ident!(
3245 "__mapcol_{}_values",
3246 data.base_name.replace('-', "_")
3247 );
3248 let base = &data.base_name;
3249 let map_ty = &data.map_ty;
3250 quote! {
3251 {
3252 let __keys = #keys_var[__i].clone().ok_or_else(|| ::std::format!(
3253 "variant `{}` row {}: map column `{}` is NA but field is required",
3254 #variant_name_str, __i, #base
3255 ))?;
3256 let __vals = #vals_var[__i].clone().ok_or_else(|| ::std::format!(
3257 "variant `{}` row {}: map column `{}` is NA but field is required",
3258 #variant_name_str, __i, #base
3259 ))?;
3260 if __keys.len() != __vals.len() {
3261 return ::core::result::Result::Err(::std::format!(
3262 "variant `{}` row {}: map column `{}` has {} keys but {} values",
3263 #variant_name_str, __i, #base, __keys.len(), __vals.len()
3264 ));
3265 }
3266 __keys.into_iter().zip(__vals).collect::<#map_ty>()
3267 }
3268 }
3269 }
3270 EnumResolvedField::Struct(data) => {
3271 let vec_var = format_ident!("__sf_{}", data.base_name.replace('-', "_"));
3272 let base = &data.base_name;
3273 quote! {
3274 #vec_var[__i].clone().ok_or_else(|| ::std::format!(
3275 "variant `{}` row {}: struct field `{}` is absent",
3276 #variant_name_str, __i, #base
3277 ))?
3278 }
3279 }
3280 }
3281 }).collect();
3282 quote! { #row_name::#variant_ident( #(#positional_exprs),* ) }
3283 }
3284 VariantShape::Unit => quote! { #row_name::#variant_ident },
3285 };
3286
3287 let _ = variant_idx; // variant_idx used logically above via contributing_variant_names
3288
3289 // Sequential arm: push onto __rows.
3290 match_arms.push(quote! {
3291 #variant_name_str => {
3292 __rows.push(#arm_body);
3293 }
3294 });
3295 // Parallel arm: return Ok(row_value).
3296 par_match_arms.push(quote! {
3297 #variant_name_str => ::core::result::Result::Ok(#arm_body),
3298 });
3299 }
3300 // endregion
3301
3302 // region: sequential body
3303 let seq_body = quote! {
3304 let __view = ::miniextendr_api::dataframe::DataFrame::from_sexp(sexp)
3305 .map_err(|e| e.to_string())?;
3306 let __nrow = __view.nrow();
3307 #(#extracts)*
3308 let mut __rows: Vec<Self> = Vec::with_capacity(__nrow);
3309 for __i in 0..__nrow {
3310 match #tag_var[__i].as_str() {
3311 #(#match_arms)*
3312 __unknown => {
3313 return ::core::result::Result::Err(::std::format!(
3314 "unknown variant tag {:?} at row {}",
3315 __unknown, __i
3316 ));
3317 }
3318 }
3319 }
3320 ::core::result::Result::Ok(__rows)
3321 };
3322 // endregion
3323
3324 // region: parallel body
3325 // For shapes with Struct fields, delegate to sequential (avoids Clone on par region).
3326 // For pure-scalar/expansion shapes, extract all columns on the R thread then
3327 // parallelize per-row dispatch over pre-extracted owned Vecs.
3328 let par_body = if has_struct_field {
3329 quote! { Self::try_from_dataframe(sexp) }
3330 } else {
3331 // Extract all columns on the R thread, then parallelize per-row dispatch.
3332 quote! {
3333 use ::miniextendr_api::rayon_bridge::rayon::prelude::*;
3334 ::miniextendr_api::optionals::parallel::ensure_pool();
3335 let __view = ::miniextendr_api::dataframe::DataFrame::from_sexp(sexp)
3336 .map_err(|e| e.to_string())?;
3337 let __nrow = __view.nrow();
3338 #(#extracts)*
3339 let __rows: Vec<Self> = (0..__nrow)
3340 .into_par_iter()
3341 .map(|__i| -> ::core::result::Result<Self, ::std::string::String> {
3342 match #tag_var[__i].as_str() {
3343 #(#par_match_arms)*
3344 __unknown => {
3345 ::core::result::Result::Err(::std::format!(
3346 "unknown variant tag {:?} at row {}",
3347 __unknown, __i
3348 ))
3349 }
3350 }
3351 })
3352 .collect::<::core::result::Result<Vec<Self>, _>>()?;
3353 ::core::result::Result::Ok(__rows)
3354 }
3355 };
3356 // endregion
3357
3358 Some(quote! {
3359 /// Read an R `data.frame` directly into a `Vec<Self>` (sequential).
3360 ///
3361 /// Reads the tag column first, then per-row dispatches to the active variant's
3362 /// field assemblers. Each schema column is pre-extracted (NA-aware, ALTREP-
3363 /// materialising). Returns `Err` with a descriptive message if a column is
3364 /// missing, mis-typed, or if an unknown tag value is encountered.
3365 ///
3366 /// This is the one-call `SEXP → Vec<Self>` reader; the same conversion is also
3367 /// reachable via the boundary-crossing [`FromDataFrame`] trait as
3368 /// `Vec::<Self>::from_dataframe(&df)`.
3369 ///
3370 /// [`FromDataFrame`]: ::miniextendr_api::dataframe::FromDataFrame
3371 pub fn try_from_dataframe(
3372 sexp: ::miniextendr_api::SEXP,
3373 ) -> ::core::result::Result<Vec<Self>, ::std::string::String> {
3374 #seq_body
3375 }
3376
3377 // api-side rayon gate (#1117).
3378 ::miniextendr_api::__dataframe_row_when_rayon! {
3379 /// Read an R `data.frame` directly into a `Vec<Self>` (parallel).
3380 ///
3381 /// Mirrors [`Self::try_from_dataframe`] but assembles rows off the R thread via
3382 /// rayon. All SEXP access happens up front on the R/worker thread; the
3383 /// `into_par_iter()` region touches only pre-extracted owned data. Shapes with
3384 /// struct-flatten/nested-enum fields delegate to the sequential reader instead.
3385 pub fn try_from_dataframe_par(
3386 sexp: ::miniextendr_api::SEXP,
3387 ) -> ::core::result::Result<Vec<Self>, ::std::string::String> {
3388 #par_body
3389 }
3390 }
3391 })
3392}
3393// endregion