Skip to main content

miniextendr_api/
markers.rs

1//! Marker traits for proc-macro derived types.
2//!
3//! These marker traits identify types that have been derived with specific proc-macros.
4//! They enable compile-time type checking and blanket implementations.
5//!
6//! # Pattern
7//!
8//! Each derive macro generates an impl of its corresponding marker trait:
9//!
10//! | Derive Macro | Marker Trait |
11//! |--------------|--------------|
12//! | `#[derive(DataFrameRow)]` | [`DataFrameRow`] |
13//! | `#[derive(PreferList)]` | [`crate::markers::PrefersList`] |
14//! | `#[derive(PreferExternalPtr)]` | [`crate::markers::PrefersExternalPtr`] |
15//! | `#[derive(PreferRNativeType)]` | [`crate::markers::PrefersRNativeType`] |
16//! | `#[derive(PreferDataFrame)]` | [`crate::markers::PrefersDataFrame`] |
17
18/// Marker trait for types generated by `#[derive(DataFrameRow)]`.
19///
20/// Automatically implemented by the `DataFrameRow` derive macro. The derive
21/// macro emits a compile-time assertion against this trait for every struct-typed
22/// variant field, giving users a clear error when the inner type is missing the
23/// derive.
24///
25/// This trait has no supertrait — the actual data-frame conversion contract is on the
26/// generated companion `{Name}DataFrame` struct (which implements `IntoDataFrame`).
27/// The marker is used solely for compile-time assertions via
28/// `_assert_inner_is_dataframe_row::<T>()` generated by the outer derive.
29///
30/// You should not implement this trait manually.
31#[diagnostic::on_unimplemented(
32    message = "the trait `DataFrameRow` is not implemented for `{Self}`",
33    label = "add `#[derive(DataFrameRow)]` to `{Self}`, annotate the field \
34             with `#[dataframe(as_list)]` to keep it as an opaque list-column, \
35             or annotate with `#[dataframe(as_factor)]` for unit-only enums",
36    note = "struct- and enum-typed variant fields are flattened by default into prefixed \
37            columns; the inner type must implement `DataFrameRow` for this to work"
38)]
39pub trait DataFrameRow {}
40
41/// Reflection trait for `#[derive(DataFrameRow)]` types, used for compile-time
42/// collision detection in outer `DataFrameRow` enums.
43///
44/// Automatically emitted by the `DataFrameRow` derive macro alongside `DataFrameRow`.
45/// The outer enum's codegen generates a `const _: ()` assertion that calls
46/// [`assert_no_payload_field_collision`] using these associated constants, producing
47/// a clear compile error when an inner payload field name (after outer prefix expansion)
48/// would produce the same R column as the outer discriminant column.
49///
50/// # Constants
51///
52/// - `FIELDS`: the resolved column names (after `#[dataframe(rename = "...")]`) that
53///   this type contributes directly. For structs: each column name. For enums: every
54///   payload field name across all variants (deduplicated).
55/// - `TAG`: the value of `#[dataframe(tag = "...")]` on this type, or `""` if absent.
56///   The outer macro uses this as the discriminant suffix for inner-enum nested fields.
57///
58/// You should not implement this trait manually.
59#[doc(hidden)]
60pub trait DataFramePayloadFields {
61    /// Resolved column names contributed by this type (post-rename, pre-prefix).
62    const FIELDS: &'static [&'static str];
63    /// Value of `#[dataframe(tag = "...")]` on this type, or `""` if absent.
64    const TAG: &'static str;
65}
66
67// region: const-eval collision helper
68
69/// Const-eval equality check for two `&str` values (stable since Rust 1.46).
70///
71/// Used by [`assert_no_payload_field_collision`] inside a `const {}` block.
72const fn const_str_eq(a: &str, b: &str) -> bool {
73    let a = a.as_bytes();
74    let b = b.as_bytes();
75    if a.len() != b.len() {
76        return false;
77    }
78    let mut i = 0;
79    while i < a.len() {
80        if a[i] != b[i] {
81            return false;
82        }
83        i += 1;
84    }
85    true
86}
87
88/// Compile-time assertion: no element of `fields` equals `discriminant_suffix`
89/// (the inner-enum's `#[dataframe(tag)]` value).
90///
91/// Called from `const _: ()` blocks emitted by the outer `DataFrameRow` derive for
92/// every `EnumResolvedField::Struct` nested-enum field:
93///
94/// ```rust,ignore
95/// const _: () = ::miniextendr_api::markers::assert_no_payload_field_collision(
96///     <Inner as ::miniextendr_api::markers::DataFramePayloadFields>::FIELDS,
97///     <Inner as ::miniextendr_api::markers::DataFramePayloadFields>::TAG,
98/// );
99/// ```
100///
101/// `fields` contains the resolved column names the inner type emits directly (not prefixed).
102/// `discriminant_suffix` is the inner enum's tag value (e.g. `"variant"`).
103///
104/// If any field name equals the discriminant suffix, the const evaluation panics with a
105/// message explaining the collision and suggesting a rename.
106#[doc(hidden)]
107pub const fn assert_no_payload_field_collision(fields: &[&str], discriminant_suffix: &str) {
108    let mut i = 0;
109    while i < fields.len() {
110        if const_str_eq(fields[i], discriminant_suffix) {
111            panic!(
112                "DataFrameRow inner-payload field collision: an inner enum payload field \
113                 has the same name as the inner enum's discriminant tag. After outer prefix \
114                 expansion this produces two columns with identical names, causing silent \
115                 data corruption. Rename the inner payload field or add \
116                 `#[dataframe(rename = \"...\")]` on it to use a different column name."
117            );
118        }
119        i += 1;
120    }
121}
122
123/// Compile-time assertion: no element of `sibling_cols` matches `concat!(base, "_", tag)`.
124///
125/// Called from `const _: ()` blocks emitted by the outer `DataFrameRow` derive for every
126/// nested-enum struct field, complementing the parse-time B1 check in `enum_expansion.rs`
127/// (which is hardcoded to the default tag `"variant"`). This assertion catches the
128/// non-default-tag case at compile time.
129///
130/// `sibling_cols` is a slice of all flat column names produced by non-Struct sibling fields
131/// in the same outer enum. `base` is the outer field name (e.g. `"kind"`). `tag` is the
132/// inner enum's `#[dataframe(tag = "...")]` value, retrieved via
133/// `<Inner as DataFramePayloadFields>::TAG`.
134///
135/// If `tag` is empty (structs emit no discriminant column), returns immediately as a no-op.
136///
137/// # Call site (emitted by proc-macro)
138///
139/// ```rust,ignore
140/// const _: () = ::miniextendr_api::markers::assert_no_sibling_field_collision(
141///     &["id", "other_col", /* … */],
142///     "kind",
143///     <Inner as ::miniextendr_api::markers::DataFramePayloadFields>::TAG,
144/// );
145/// ```
146#[doc(hidden)]
147pub const fn assert_no_sibling_field_collision(sibling_cols: &[&str], base: &str, tag: &str) {
148    // Structs don't emit a discriminant column, so tag == "" → no-op.
149    if tag.is_empty() {
150        return;
151    }
152    let expected_len = base.len() + 1 + tag.len();
153    let base_bytes = base.as_bytes();
154    let tag_bytes = tag.as_bytes();
155    let mut i = 0;
156    while i < sibling_cols.len() {
157        let col = sibling_cols[i].as_bytes();
158        if col.len() == expected_len {
159            // Check base prefix byte-by-byte.
160            let mut j = 0;
161            let mut base_match = true;
162            while j < base_bytes.len() {
163                if col[j] != base_bytes[j] {
164                    base_match = false;
165                    break;
166                }
167                j += 1;
168            }
169            // Check separator '_'.
170            let sep_match = col[base_bytes.len()] == b'_';
171            // Check tag suffix byte-by-byte.
172            let mut k = 0;
173            let mut tag_match = true;
174            let offset = base_bytes.len() + 1;
175            while k < tag_bytes.len() {
176                if col[offset + k] != tag_bytes[k] {
177                    tag_match = false;
178                    break;
179                }
180                k += 1;
181            }
182            if base_match && sep_match && tag_match {
183                panic!(
184                    "DataFrameRow B1 sibling-collision: a sibling field in the outer enum \
185                     produces a column name that collides with the discriminant column emitted \
186                     by a nested inner enum. Rename the sibling field or use \
187                     `#[dataframe(tag = \"...\")]` on the inner enum to choose a different \
188                     discriminant column name."
189                );
190            }
191        }
192        i += 1;
193    }
194}
195// endregion
196
197/// Marker trait for types that should be converted to R lists via `IntoR`.
198///
199/// Implemented by the `PreferList` derive; you can also implement it manually.
200pub trait PrefersList: crate::list::IntoList {}
201
202/// Marker trait for types that should be converted to R data frames via `IntoR`.
203///
204/// Implemented by the `PreferDataFrame` derive; you can also implement it manually.
205pub trait PrefersDataFrame: crate::convert::IntoDataFrame {}
206
207/// Marker trait for types that prefer `ExternalPtr` conversion.
208///
209/// Implemented by the `PreferExternalPtr` derive; currently informational.
210pub trait PrefersExternalPtr: crate::externalptr::IntoExternalPtr {}
211
212/// Marker trait for types that prefer native SEXP conversion.
213///
214/// Implemented by the `PreferRNativeType` derive; currently informational.
215pub trait PrefersRNativeType: crate::ffi::RNativeType {}
216
217// region: Coercion marker traits
218
219/// Marker trait for types that can widen to `i32` without loss.
220///
221/// Manually implemented for specific types to avoid conflicts with identity/
222/// special-case conversions. Used by blanket Coerce implementations.
223pub trait WidensToI32: Into<i32> + Copy {}
224
225/// Marker trait for types that can widen to `f64` without loss.
226///
227/// Manually implemented for specific types to avoid conflicts with identity/
228/// special-case conversions. Used by blanket Coerce implementations.
229pub trait WidensToF64: Into<f64> + Copy {}
230
231// Explicit marker impls for widening conversions (no blanket impl to avoid conflicts)
232impl WidensToI32 for i8 {}
233impl WidensToI32 for i16 {}
234impl WidensToI32 for u8 {}
235impl WidensToI32 for u16 {}
236
237impl WidensToF64 for f32 {}
238impl WidensToF64 for i8 {}
239impl WidensToF64 for i16 {}
240impl WidensToF64 for i32 {}
241impl WidensToF64 for u8 {}
242impl WidensToF64 for u16 {}
243impl WidensToF64 for u32 {}
244// endregion