Rust · 10659 bytes Raw Blame History
1 //! Array and string descriptors — the ABI contract between codegen and runtime.
2 //!
3 //! These repr(C) structs define the binary layout that generated ARM64 code
4 //! reads/writes directly. The runtime functions operate on pointers to these
5 //! descriptors. **This layout is stable once committed** — changing it requires
6 //! recompiling all Fortran code.
7 //!
8 //! Design choices:
9 //! - Max rank 15 (Fortran standard allows up to 15 dimensions)
10 //! - Stride in elements (not bytes) — multiply by elem_size for byte offset
11 //! - Flags bitfield for allocated/contiguous/pointer status
12 //! - Fixed-size dims array (no heap allocation for the descriptor itself)
13
14 use std::ptr;
15
16 /// Maximum array rank (Fortran 2018 allows up to 15).
17 pub const MAX_RANK: usize = 15;
18
19 /// Descriptor flags.
20 pub const DESC_ALLOCATED: u32 = 1 << 0;
21 pub const DESC_CONTIGUOUS: u32 = 1 << 1;
22 pub const DESC_POINTER: u32 = 1 << 2;
23
24 /// Array descriptor — the runtime representation of a Fortran array.
25 ///
26 /// Layout:
27 /// ```text
28 /// offset field size
29 /// 0 base_addr 8 bytes (pointer)
30 /// 8 elem_size 8 bytes (i64)
31 /// 16 rank 4 bytes (i32)
32 /// 20 flags 4 bytes (u32)
33 /// 24 dims[0] 24 bytes (DimDescriptor)
34 /// 48 dims[1] 24 bytes
35 /// ...
36 /// 384 dims[14] 24 bytes
37 /// total: 384 bytes
38 /// ```
39 #[repr(C)]
40 #[derive(Clone)]
41 pub struct ArrayDescriptor {
42 /// Pointer to the first element of the array data.
43 pub base_addr: *mut u8,
44 /// Size of one element in bytes.
45 pub elem_size: i64,
46 /// Number of dimensions (1-15).
47 pub rank: i32,
48 /// Status flags: allocated, contiguous, pointer.
49 pub flags: u32,
50 /// Per-dimension information (lower bound, upper bound, stride).
51 pub dims: [DimDescriptor; MAX_RANK],
52 }
53
54 /// Per-dimension descriptor.
55 #[repr(C)]
56 #[derive(Clone, Copy, Default)]
57 pub struct DimDescriptor {
58 /// Lower bound (inclusive). Default is 1 per Fortran convention.
59 pub lower_bound: i64,
60 /// Upper bound (inclusive).
61 pub upper_bound: i64,
62 /// Stride in elements (not bytes). 1 for contiguous, >1 for sections.
63 pub stride: i64,
64 }
65
66 impl DimDescriptor {
67 /// Number of elements along this dimension.
68 ///
69 /// Throughout this codebase a `DimDescriptor`'s `(lower_bound,
70 /// upper_bound)` are the section's logical 1-based bounds and the
71 /// `stride` field records the *memory* step (in elements) between
72 /// adjacent logical positions — see `afs_create_section` and the
73 /// matching IR loop body in `lower_array_assign`. The IR-level
74 /// extent computation at lower.rs:28443 already uses
75 /// `upper - lower + 1` and ignores stride; this Rust-side formula
76 /// is the runtime mirror, so it must do the same.
77 pub fn extent(&self) -> i64 {
78 if self.upper_bound < self.lower_bound {
79 0
80 } else {
81 self.upper_bound - self.lower_bound + 1
82 }
83 }
84 }
85
86 impl ArrayDescriptor {
87 /// Create a zeroed (unallocated) descriptor.
88 pub fn zeroed() -> Self {
89 Self {
90 base_addr: ptr::null_mut(),
91 elem_size: 0,
92 rank: 0,
93 flags: 0,
94 dims: [DimDescriptor::default(); MAX_RANK],
95 }
96 }
97
98 /// Check if the array is currently allocated.
99 pub fn is_allocated(&self) -> bool {
100 self.flags & DESC_ALLOCATED != 0
101 }
102
103 /// Check if the array is contiguous in memory.
104 pub fn is_contiguous(&self) -> bool {
105 self.flags & DESC_CONTIGUOUS != 0
106 }
107
108 /// Total number of elements across all dimensions.
109 pub fn total_elements(&self) -> i64 {
110 let mut total: i64 = 1;
111 for i in 0..self.rank as usize {
112 total *= self.dims[i].extent();
113 }
114 total
115 }
116
117 /// Total size in bytes: total_elements * elem_size.
118 pub fn total_bytes(&self) -> i64 {
119 self.total_elements() * self.elem_size
120 }
121
122 /// Compute the byte offset for an element given subscripts (0-based indices).
123 /// Column-major order: first index varies fastest.
124 pub fn element_offset(&self, subscripts: &[i64]) -> i64 {
125 let mut offset: i64 = 0;
126 let mut multiplier: i64 = 1;
127 for i in 0..self.rank as usize {
128 let idx = if i < subscripts.len() {
129 subscripts[i] - self.dims[i].lower_bound
130 } else {
131 0
132 };
133 offset += idx * multiplier * self.dims[i].stride;
134 multiplier *= self.dims[i].extent();
135 }
136 offset * self.elem_size
137 }
138
139 /// Set dimensions from a slice of (lower, upper) bounds.
140 /// Assumes contiguous layout with stride=1 for each dimension.
141 pub fn set_bounds(&mut self, bounds: &[(i64, i64)]) {
142 self.rank = bounds.len() as i32;
143 for (i, &(lo, hi)) in bounds.iter().enumerate() {
144 self.dims[i] = DimDescriptor {
145 lower_bound: lo,
146 upper_bound: hi,
147 stride: 1,
148 };
149 }
150 self.flags |= DESC_CONTIGUOUS;
151 }
152
153 /// Runtime concrete-type tag for scalar polymorphic allocatables.
154 ///
155 /// The current ABI stores this in `dims[0].lower_bound` when `rank == 0`.
156 pub fn scalar_type_tag(&self) -> i64 {
157 if self.rank == 0 {
158 self.dims[0].lower_bound
159 } else {
160 0
161 }
162 }
163
164 pub fn set_scalar_type_tag(&mut self, tag: i64) {
165 if self.rank == 0 {
166 self.dims[0].lower_bound = tag;
167 self.dims[0].stride = 0;
168 }
169 }
170
171 pub fn scalar_tbp_lookup_ptr(&self) -> *mut u8 {
172 if self.rank == 0 {
173 self.dims[0].upper_bound as usize as *mut u8
174 } else {
175 ptr::null_mut()
176 }
177 }
178
179 pub fn set_scalar_tbp_lookup_ptr(&mut self, ptr: *mut u8) {
180 if self.rank == 0 {
181 self.dims[0].upper_bound = ptr as usize as i64;
182 self.dims[0].stride = 0;
183 }
184 }
185
186 pub fn clear_scalar_type_tag(&mut self) {
187 if self.rank == 0 {
188 self.dims[0] = DimDescriptor::default();
189 }
190 }
191 }
192
193 /// String descriptor — the runtime representation of a Fortran character variable.
194 ///
195 /// Layout:
196 /// ```text
197 /// offset field size
198 /// 0 data 8 bytes (pointer)
199 /// 8 len 8 bytes (i64, length in characters)
200 /// 16 capacity 8 bytes (i64, allocated bytes, for deferred-length)
201 /// 24 flags 4 bytes (u32)
202 /// total: 32 bytes (padded to 32 for alignment)
203 /// ```
204 #[repr(C)]
205 #[derive(Clone)]
206 pub struct StringDescriptor {
207 /// Pointer to the character data (not null-terminated).
208 pub data: *mut u8,
209 /// Current length in characters (= bytes for kind=1).
210 pub len: i64,
211 /// Allocated capacity in bytes. For fixed-length strings, capacity == len.
212 /// For deferred-length (allocatable), capacity >= len.
213 pub capacity: i64,
214 /// Flags: allocated, deferred-length, etc.
215 pub flags: u32,
216 }
217
218 /// String descriptor flags.
219 pub const STR_ALLOCATED: u32 = 1 << 0;
220 pub const STR_DEFERRED: u32 = 1 << 1;
221
222 impl StringDescriptor {
223 /// Create a zeroed (empty) string descriptor.
224 pub fn zeroed() -> Self {
225 Self {
226 data: ptr::null_mut(),
227 len: 0,
228 capacity: 0,
229 flags: 0,
230 }
231 }
232
233 /// Create a descriptor for a fixed-length string (stack-allocated data).
234 pub fn fixed(data: *mut u8, len: i64) -> Self {
235 Self {
236 data,
237 len,
238 capacity: len,
239 flags: 0,
240 }
241 }
242
243 /// Check if the string is allocated (deferred-length).
244 pub fn is_allocated(&self) -> bool {
245 self.flags & STR_ALLOCATED != 0
246 }
247
248 /// Get the string data as a byte slice.
249 pub unsafe fn as_bytes(&self) -> &[u8] {
250 if self.data.is_null() || self.len <= 0 {
251 &[]
252 } else {
253 std::slice::from_raw_parts(self.data, self.len as usize)
254 }
255 }
256 }
257
258 #[cfg(test)]
259 mod tests {
260 use super::*;
261
262 #[test]
263 fn descriptor_zeroed() {
264 let d = ArrayDescriptor::zeroed();
265 assert!(!d.is_allocated());
266 assert_eq!(d.rank, 0);
267 assert!(d.base_addr.is_null());
268 }
269
270 #[test]
271 fn dim_extent() {
272 let dim = DimDescriptor {
273 lower_bound: 1,
274 upper_bound: 10,
275 stride: 1,
276 };
277 assert_eq!(dim.extent(), 10);
278
279 // Memory-stride convention: bounds are the section's logical
280 // 1-based positions and stride is the inter-element memory
281 // step. extent is `upper - lower + 1`, independent of stride.
282 let dim2 = DimDescriptor {
283 lower_bound: 1,
284 upper_bound: 5,
285 stride: 2,
286 };
287 assert_eq!(dim2.extent(), 5);
288
289 let dim3 = DimDescriptor {
290 lower_bound: 5,
291 upper_bound: 3,
292 stride: 1,
293 };
294 assert_eq!(dim3.extent(), 0); // empty
295 }
296
297 #[test]
298 fn total_elements() {
299 let mut d = ArrayDescriptor::zeroed();
300 d.set_bounds(&[(1, 10), (1, 20)]);
301 assert_eq!(d.total_elements(), 200);
302 assert_eq!(d.rank, 2);
303 assert!(d.is_contiguous());
304 }
305
306 #[test]
307 fn element_offset_1d() {
308 let mut d = ArrayDescriptor::zeroed();
309 d.elem_size = 4; // i32
310 d.set_bounds(&[(1, 10)]);
311 // a(1) → offset 0, a(2) → offset 4, a(10) → offset 36
312 assert_eq!(d.element_offset(&[1]), 0);
313 assert_eq!(d.element_offset(&[2]), 4);
314 assert_eq!(d.element_offset(&[10]), 36);
315 }
316
317 #[test]
318 fn element_offset_2d_column_major() {
319 let mut d = ArrayDescriptor::zeroed();
320 d.elem_size = 8; // f64
321 d.set_bounds(&[(1, 3), (1, 4)]); // 3x4 matrix
322 // Column-major: a(1,1)=0, a(2,1)=8, a(3,1)=16, a(1,2)=24
323 assert_eq!(d.element_offset(&[1, 1]), 0);
324 assert_eq!(d.element_offset(&[2, 1]), 8);
325 assert_eq!(d.element_offset(&[3, 1]), 16);
326 assert_eq!(d.element_offset(&[1, 2]), 24);
327 assert_eq!(d.element_offset(&[3, 4]), 88); // last element
328 }
329
330 #[test]
331 fn string_descriptor() {
332 let s = StringDescriptor::zeroed();
333 assert!(!s.is_allocated());
334 assert_eq!(s.len, 0);
335 assert!(s.data.is_null());
336 }
337
338 #[test]
339 fn descriptor_size_is_stable() {
340 // ABI stability: descriptor sizes must not change.
341 assert_eq!(std::mem::size_of::<DimDescriptor>(), 24);
342 assert_eq!(std::mem::size_of::<ArrayDescriptor>(), 384);
343 assert_eq!(std::mem::size_of::<StringDescriptor>(), 32);
344 }
345 }
346