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