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` / `PreferDataFrame` / `PreferExternalPtr` /
15//! `PreferRNativeType`) emit their `IntoR` impl directly and carry **no** companion marker
16//! trait — a type-level representation default is just "which `IntoR` impl the derive wrote",
17//! not a tag anything dispatches on. (Four marker-keyed `IntoR` blankets would mutually
18//! overlap under coherence anyway, so the markers could never become load-bearing.)
19
20/// Marker trait for types generated by `#[derive(DataFrameRow)]`.
21///
22/// Automatically implemented by the `DataFrameRow` derive macro. The derive
23/// macro emits a compile-time assertion against this trait for every struct-typed
24/// variant field, giving users a clear error when the inner type is missing the
25/// derive.
26///
27/// This trait has no supertrait — the actual data-frame conversion contract is on the
28/// generated companion `{Name}DataFrame` struct (which implements `ColumnSource`).
29/// The marker is used solely for compile-time assertions via
30/// `_assert_inner_is_dataframe_row::<T>()` generated by the outer derive.
31///
32/// You should not implement this trait manually.
33#[diagnostic::on_unimplemented(
34    message = "the trait `DataFrameRow` is not implemented for `{Self}`",
35    label = "add `#[derive(DataFrameRow)]` to `{Self}`, annotate the field \
36             with `#[dataframe(as_list)]` to keep it as an opaque list-column, \
37             or annotate with `#[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            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                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