| 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 |