Skip to main content

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