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