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}