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