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}