Skip to main content

ferray_core/
record.rs

1// ferray-core: FerrumRecord support types (REQ-8 prep)
2//
3// This module defines the traits and types that `#[derive(FerrumRecord)]`
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(FerrumRecord)]` 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 FerrumRecord: 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    fn field_by_name(name: &str) -> Option<&'static FieldDescriptor> {
42        Self::field_descriptors().iter().find(|fd| fd.name == name)
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    // Manual implementation to verify the trait works before the proc macro exists.
51    #[repr(C)]
52    #[derive(Clone, Debug)]
53    struct TestRecord {
54        x: f64,
55        y: f64,
56        label: i32,
57    }
58
59    // In real usage, #[derive(FerrumRecord)] generates this.
60    unsafe impl FerrumRecord for TestRecord {
61        fn field_descriptors() -> &'static [FieldDescriptor] {
62            static FIELDS: [FieldDescriptor; 3] = [
63                FieldDescriptor {
64                    name: "x",
65                    dtype: DType::F64,
66                    offset: 0,
67                    size: 8,
68                },
69                FieldDescriptor {
70                    name: "y",
71                    dtype: DType::F64,
72                    offset: 8,
73                    size: 8,
74                },
75                FieldDescriptor {
76                    name: "label",
77                    dtype: DType::I32,
78                    offset: 16,
79                    size: 4,
80                },
81            ];
82            &FIELDS
83        }
84
85        fn record_size() -> usize {
86            core::mem::size_of::<Self>()
87        }
88    }
89
90    #[test]
91    fn record_field_descriptors() {
92        let fields = TestRecord::field_descriptors();
93        assert_eq!(fields.len(), 3);
94        assert_eq!(fields[0].name, "x");
95        assert_eq!(fields[0].dtype, DType::F64);
96        assert_eq!(fields[1].name, "y");
97        assert_eq!(fields[2].name, "label");
98        assert_eq!(fields[2].dtype, DType::I32);
99    }
100
101    #[test]
102    fn record_field_by_name() {
103        let fd = TestRecord::field_by_name("y").unwrap();
104        assert_eq!(fd.dtype, DType::F64);
105        assert_eq!(fd.offset, 8);
106
107        assert!(TestRecord::field_by_name("nonexistent").is_none());
108    }
109
110    #[test]
111    fn record_size() {
112        assert!(TestRecord::record_size() >= 20); // at least 8+8+4, may have padding
113    }
114}