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