Skip to main content

ferray_core/
record.rs

1// ferray-core: FerrayRecord support types (REQ-8 prep)
2//
3// This module defines the traits and types that `#[derive(FerrayRecord)]`
4// (implemented by Agent 1d in ferray-core-macros) will generate impls for.
5// The proc macro itself is NOT implemented here.
6
7use crate::dtype::DType;
8
9/// Describes a single field within a structured (record) dtype.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct FieldDescriptor {
12    /// Name of the field.
13    pub name: &'static str,
14    /// The scalar dtype of this field.
15    pub dtype: DType,
16    /// Byte offset of this field within the record.
17    pub offset: usize,
18    /// Size in bytes of this field.
19    pub size: usize,
20}
21
22/// Trait implemented by types that can be used as structured array elements.
23///
24/// `#[derive(FerrayRecord)]` generates this implementation automatically.
25/// It provides the field descriptors needed for zero-copy strided views
26/// of individual fields within an array of structs.
27///
28/// # Safety
29/// Implementors must ensure that:
30/// - The struct is `#[repr(C)]` (no field reordering by the compiler).
31/// - All fields implement [`Element`](crate::dtype::Element).
32/// - `field_descriptors()` accurately reflects the struct layout.
33pub unsafe trait FerrayRecord: Clone + Send + Sync + 'static {
34    /// Return descriptors for all fields, in declaration order.
35    fn field_descriptors() -> &'static [FieldDescriptor];
36
37    /// Total size of one record in bytes (same as `core::mem::size_of::<Self>()`).
38    fn record_size() -> usize;
39
40    /// Return the field descriptor for a named field, if it exists.
41    #[must_use]
42    fn field_by_name(name: &str) -> Option<&'static FieldDescriptor> {
43        Self::field_descriptors().iter().find(|fd| fd.name == name)
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    // Manual implementation to verify the trait works before the proc macro exists.
52    #[repr(C)]
53    #[derive(Clone, Debug)]
54    struct TestRecord {
55        x: f64,
56        y: f64,
57        label: i32,
58    }
59
60    // In real usage, #[derive(FerrayRecord)] generates this.
61    unsafe impl FerrayRecord for TestRecord {
62        fn field_descriptors() -> &'static [FieldDescriptor] {
63            static FIELDS: [FieldDescriptor; 3] = [
64                FieldDescriptor {
65                    name: "x",
66                    dtype: DType::F64,
67                    offset: 0,
68                    size: 8,
69                },
70                FieldDescriptor {
71                    name: "y",
72                    dtype: DType::F64,
73                    offset: 8,
74                    size: 8,
75                },
76                FieldDescriptor {
77                    name: "label",
78                    dtype: DType::I32,
79                    offset: 16,
80                    size: 4,
81                },
82            ];
83            &FIELDS
84        }
85
86        fn record_size() -> usize {
87            core::mem::size_of::<Self>()
88        }
89    }
90
91    #[test]
92    fn record_field_descriptors() {
93        let fields = TestRecord::field_descriptors();
94        assert_eq!(fields.len(), 3);
95        assert_eq!(fields[0].name, "x");
96        assert_eq!(fields[0].dtype, DType::F64);
97        assert_eq!(fields[1].name, "y");
98        assert_eq!(fields[2].name, "label");
99        assert_eq!(fields[2].dtype, DType::I32);
100    }
101
102    #[test]
103    fn record_field_by_name() {
104        let fd = TestRecord::field_by_name("y").unwrap();
105        assert_eq!(fd.dtype, DType::F64);
106        assert_eq!(fd.offset, 8);
107
108        assert!(TestRecord::field_by_name("nonexistent").is_none());
109    }
110
111    #[test]
112    fn record_size() {
113        assert!(TestRecord::record_size() >= 20); // at least 8+8+4, may have padding
114    }
115}