shape_value/v2_struct_layout.rs
1//! Compile-time `repr(C)` struct layout computation for the v2 runtime.
2//!
3//! Given a type definition like `type Point { x: number, y: number }`, this module
4//! computes the exact byte layout with field offsets as compile-time constants:
5//!
6//! ```text
7//! #[repr(C)]
8//! struct PointLayout {
9//! header: HeapHeader, // 8 bytes (offset 0)
10//! x: f64, // 8 bytes (offset 8)
11//! y: f64, // 8 bytes (offset 16)
12//! }
13//! ```
14//!
15//! Field access compiles to a direct load at a known offset: `point.x` becomes
16//! `load f64 [ptr + 8]`. No schema lookup, no HashMap, no runtime dispatch.
17
18/// Primitive field types with known sizes and alignments.
19///
20/// These map directly to machine types the JIT can emit loads/stores for.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum FieldType {
23 /// 64-bit IEEE 754 float (`number` in Shape). 8 bytes, 8-byte aligned.
24 F64,
25 /// 64-bit signed integer. 8 bytes, 8-byte aligned.
26 I64,
27 /// 32-bit signed integer. 4 bytes, 4-byte aligned.
28 I32,
29 /// 32-bit unsigned integer. 4 bytes, 4-byte aligned.
30 U32,
31 /// 16-bit signed integer. 2 bytes, 2-byte aligned.
32 I16,
33 /// 16-bit unsigned integer. 2 bytes, 2-byte aligned.
34 U16,
35 /// 8-bit signed integer. 1 byte, 1-byte aligned.
36 I8,
37 /// 8-bit unsigned integer. 1 byte, 1-byte aligned.
38 U8,
39 /// Boolean. 1 byte, 1-byte aligned.
40 Bool,
41 /// Pointer to a heap object. 8 bytes, 8-byte aligned.
42 Ptr,
43}
44
45impl FieldType {
46 /// Size of this field type in bytes.
47 #[inline]
48 pub const fn size(self) -> u32 {
49 match self {
50 FieldType::F64 | FieldType::I64 | FieldType::Ptr => 8,
51 FieldType::I32 | FieldType::U32 => 4,
52 FieldType::I16 | FieldType::U16 => 2,
53 FieldType::I8 | FieldType::U8 | FieldType::Bool => 1,
54 }
55 }
56
57 /// Natural alignment of this field type in bytes.
58 #[inline]
59 pub const fn align(self) -> u32 {
60 // Natural alignment: size == alignment for all primitive types.
61 self.size()
62 }
63}
64
65/// Layout information for a single field within a struct.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct StructFieldLayout {
68 /// Field name (e.g. `"x"`).
69 pub name: String,
70 /// Byte offset from the start of the struct (including header).
71 pub offset: u32,
72 /// Size of this field in bytes.
73 pub size: u32,
74 /// The primitive type of this field.
75 pub field_type: FieldType,
76}
77
78/// Complete layout of a `repr(C)` struct including its v2 heap header.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct StructLayout {
81 /// Type name (e.g. `"Point"`).
82 pub name: String,
83 /// Total size of the struct in bytes, including header and tail padding.
84 /// Padded to the struct's overall alignment.
85 pub total_size: u32,
86 /// Per-field layout information, in declaration order.
87 pub fields: Vec<StructFieldLayout>,
88}
89
90/// Size of the v2 heap header in bytes.
91///
92/// The v2 runtime uses a compact 8-byte header at offset 0 of every heap object.
93/// This is distinct from the v1 `HeapHeader` (32 bytes). The v2 header packs
94/// kind, flags, and auxiliary data into 8 bytes.
95pub const V2_HEADER_SIZE: u32 = 8;
96
97/// Alignment of the v2 heap header.
98pub const V2_HEADER_ALIGN: u32 = 8;
99
100/// Round `offset` up to the next multiple of `align`.
101///
102/// `align` must be a power of two.
103#[inline]
104const fn align_up(offset: u32, align: u32) -> u32 {
105 debug_assert!(align.is_power_of_two());
106 let mask = align - 1;
107 (offset + mask) & !mask
108}
109
110/// Compute the `repr(C)` struct layout for a named type with the given fields.
111///
112/// The layout follows C struct rules:
113/// 1. An 8-byte v2 heap header occupies bytes `[0, 8)`.
114/// 2. Fields are placed sequentially after the header, each aligned to its
115/// natural alignment (inserting padding bytes as needed).
116/// 3. The total size is padded to the struct's overall alignment (the maximum
117/// alignment of the header and all fields).
118///
119/// # Examples
120///
121/// ```
122/// use shape_value::v2_struct_layout::{compute_struct_layout, FieldType};
123///
124/// let layout = compute_struct_layout("Point", &[
125/// ("x".into(), FieldType::F64),
126/// ("y".into(), FieldType::F64),
127/// ]);
128/// assert_eq!(layout.fields[0].offset, 8); // after 8-byte header
129/// assert_eq!(layout.fields[1].offset, 16);
130/// assert_eq!(layout.total_size, 24);
131/// ```
132pub fn compute_struct_layout(name: &str, fields: &[(String, FieldType)]) -> StructLayout {
133 // Track the maximum alignment across header and all fields.
134 let mut max_align = V2_HEADER_ALIGN;
135 // Current write cursor starts after the header.
136 let mut cursor = V2_HEADER_SIZE;
137
138 let mut field_layouts = Vec::with_capacity(fields.len());
139
140 for (field_name, field_type) in fields {
141 let size = field_type.size();
142 let align = field_type.align();
143
144 // Update struct-wide max alignment.
145 if align > max_align {
146 max_align = align;
147 }
148
149 // Align cursor for this field.
150 cursor = align_up(cursor, align);
151
152 field_layouts.push(StructFieldLayout {
153 name: field_name.clone(),
154 offset: cursor,
155 size,
156 field_type: *field_type,
157 });
158
159 cursor += size;
160 }
161
162 // Pad total size to struct alignment (C layout rule).
163 let total_size = align_up(cursor, max_align);
164
165 StructLayout {
166 name: name.to_string(),
167 total_size,
168 fields: field_layouts,
169 }
170}
171
172impl StructLayout {
173 /// Look up a field by name, returning its layout if found.
174 pub fn field(&self, name: &str) -> Option<&StructFieldLayout> {
175 self.fields.iter().find(|f| f.name == name)
176 }
177
178 /// The overall alignment of this struct (max of header and field alignments).
179 pub fn alignment(&self) -> u32 {
180 let mut max_align = V2_HEADER_ALIGN;
181 for f in &self.fields {
182 let a = f.field_type.align();
183 if a > max_align {
184 max_align = a;
185 }
186 }
187 max_align
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 // -----------------------------------------------------------------------
196 // FieldType basics
197 // -----------------------------------------------------------------------
198
199 #[test]
200 fn test_field_type_sizes() {
201 assert_eq!(FieldType::F64.size(), 8);
202 assert_eq!(FieldType::I64.size(), 8);
203 assert_eq!(FieldType::Ptr.size(), 8);
204 assert_eq!(FieldType::I32.size(), 4);
205 assert_eq!(FieldType::U32.size(), 4);
206 assert_eq!(FieldType::I16.size(), 2);
207 assert_eq!(FieldType::U16.size(), 2);
208 assert_eq!(FieldType::I8.size(), 1);
209 assert_eq!(FieldType::U8.size(), 1);
210 assert_eq!(FieldType::Bool.size(), 1);
211 }
212
213 #[test]
214 fn test_field_type_alignments() {
215 assert_eq!(FieldType::F64.align(), 8);
216 assert_eq!(FieldType::I64.align(), 8);
217 assert_eq!(FieldType::Ptr.align(), 8);
218 assert_eq!(FieldType::I32.align(), 4);
219 assert_eq!(FieldType::U32.align(), 4);
220 assert_eq!(FieldType::I16.align(), 2);
221 assert_eq!(FieldType::U16.align(), 2);
222 assert_eq!(FieldType::I8.align(), 1);
223 assert_eq!(FieldType::U8.align(), 1);
224 assert_eq!(FieldType::Bool.align(), 1);
225 }
226
227 // -----------------------------------------------------------------------
228 // align_up helper
229 // -----------------------------------------------------------------------
230
231 #[test]
232 fn test_align_up() {
233 assert_eq!(align_up(0, 8), 0);
234 assert_eq!(align_up(1, 8), 8);
235 assert_eq!(align_up(7, 8), 8);
236 assert_eq!(align_up(8, 8), 8);
237 assert_eq!(align_up(9, 8), 16);
238 assert_eq!(align_up(3, 4), 4);
239 assert_eq!(align_up(4, 4), 4);
240 assert_eq!(align_up(5, 4), 8);
241 assert_eq!(align_up(0, 1), 0);
242 assert_eq!(align_up(7, 1), 7);
243 assert_eq!(align_up(1, 2), 2);
244 assert_eq!(align_up(2, 2), 2);
245 assert_eq!(align_up(3, 2), 4);
246 }
247
248 // -----------------------------------------------------------------------
249 // Point { x: f64, y: f64 }
250 // -----------------------------------------------------------------------
251
252 #[test]
253 fn test_point_layout() {
254 let layout = compute_struct_layout("Point", &[
255 ("x".into(), FieldType::F64),
256 ("y".into(), FieldType::F64),
257 ]);
258
259 assert_eq!(layout.name, "Point");
260 assert_eq!(layout.fields.len(), 2);
261
262 // x: f64 at offset 8 (right after 8-byte header)
263 assert_eq!(layout.fields[0].name, "x");
264 assert_eq!(layout.fields[0].offset, 8);
265 assert_eq!(layout.fields[0].size, 8);
266 assert_eq!(layout.fields[0].field_type, FieldType::F64);
267
268 // y: f64 at offset 16
269 assert_eq!(layout.fields[1].name, "y");
270 assert_eq!(layout.fields[1].offset, 16);
271 assert_eq!(layout.fields[1].size, 8);
272 assert_eq!(layout.fields[1].field_type, FieldType::F64);
273
274 // Total: header(8) + x(8) + y(8) = 24, aligned to 8 = 24
275 assert_eq!(layout.total_size, 24);
276 assert_eq!(layout.alignment(), 8);
277 }
278
279 // -----------------------------------------------------------------------
280 // Color { r: u8, g: u8, b: u8 }
281 // -----------------------------------------------------------------------
282
283 #[test]
284 fn test_color_layout() {
285 let layout = compute_struct_layout("Color", &[
286 ("r".into(), FieldType::U8),
287 ("g".into(), FieldType::U8),
288 ("b".into(), FieldType::U8),
289 ]);
290
291 assert_eq!(layout.name, "Color");
292 assert_eq!(layout.fields.len(), 3);
293
294 // r: u8 at offset 8 (right after header, 1-byte aligned — no padding)
295 assert_eq!(layout.fields[0].name, "r");
296 assert_eq!(layout.fields[0].offset, 8);
297 assert_eq!(layout.fields[0].size, 1);
298
299 // g: u8 at offset 9
300 assert_eq!(layout.fields[1].name, "g");
301 assert_eq!(layout.fields[1].offset, 9);
302 assert_eq!(layout.fields[1].size, 1);
303
304 // b: u8 at offset 10
305 assert_eq!(layout.fields[2].name, "b");
306 assert_eq!(layout.fields[2].offset, 10);
307 assert_eq!(layout.fields[2].size, 1);
308
309 // Total: header(8) + r(1) + g(1) + b(1) = 11, padded to max_align(8) = 16
310 assert_eq!(layout.total_size, 16);
311 assert_eq!(layout.alignment(), 8);
312 }
313
314 // -----------------------------------------------------------------------
315 // Mixed types with alignment padding
316 // -----------------------------------------------------------------------
317
318 #[test]
319 fn test_mixed_alignment_padding() {
320 // Simulates: type Mixed { flag: bool, value: f64, count: i32 }
321 let layout = compute_struct_layout("Mixed", &[
322 ("flag".into(), FieldType::Bool),
323 ("value".into(), FieldType::F64),
324 ("count".into(), FieldType::I32),
325 ]);
326
327 assert_eq!(layout.fields.len(), 3);
328
329 // flag: bool at offset 8 (1 byte)
330 assert_eq!(layout.fields[0].name, "flag");
331 assert_eq!(layout.fields[0].offset, 8);
332 assert_eq!(layout.fields[0].size, 1);
333
334 // value: f64 at offset 16 (needs 8-byte alignment, so 7 bytes of padding after flag)
335 assert_eq!(layout.fields[1].name, "value");
336 assert_eq!(layout.fields[1].offset, 16);
337 assert_eq!(layout.fields[1].size, 8);
338
339 // count: i32 at offset 24 (needs 4-byte alignment, already aligned)
340 assert_eq!(layout.fields[2].name, "count");
341 assert_eq!(layout.fields[2].offset, 24);
342 assert_eq!(layout.fields[2].size, 4);
343
344 // Total: 24 + 4 = 28, padded to 8 = 32
345 assert_eq!(layout.total_size, 32);
346 assert_eq!(layout.alignment(), 8);
347 }
348
349 // -----------------------------------------------------------------------
350 // Empty struct (header only)
351 // -----------------------------------------------------------------------
352
353 #[test]
354 fn test_empty_struct() {
355 let layout = compute_struct_layout("Empty", &[]);
356
357 assert_eq!(layout.name, "Empty");
358 assert_eq!(layout.fields.len(), 0);
359 // Just the header, padded to header alignment
360 assert_eq!(layout.total_size, 8);
361 assert_eq!(layout.alignment(), 8);
362 }
363
364 // -----------------------------------------------------------------------
365 // Single field
366 // -----------------------------------------------------------------------
367
368 #[test]
369 fn test_single_bool_field() {
370 let layout = compute_struct_layout("Flag", &[
371 ("value".into(), FieldType::Bool),
372 ]);
373
374 assert_eq!(layout.fields[0].offset, 8);
375 assert_eq!(layout.fields[0].size, 1);
376 // header(8) + bool(1) = 9, padded to 8 = 16
377 assert_eq!(layout.total_size, 16);
378 }
379
380 #[test]
381 fn test_single_i32_field() {
382 let layout = compute_struct_layout("Counter", &[
383 ("n".into(), FieldType::I32),
384 ]);
385
386 assert_eq!(layout.fields[0].offset, 8);
387 assert_eq!(layout.fields[0].size, 4);
388 // header(8) + i32(4) = 12, padded to 8 = 16
389 assert_eq!(layout.total_size, 16);
390 }
391
392 // -----------------------------------------------------------------------
393 // All field types in sequence
394 // -----------------------------------------------------------------------
395
396 #[test]
397 fn test_all_field_types() {
398 let layout = compute_struct_layout("AllTypes", &[
399 ("a_f64".into(), FieldType::F64),
400 ("b_i64".into(), FieldType::I64),
401 ("c_ptr".into(), FieldType::Ptr),
402 ("d_i32".into(), FieldType::I32),
403 ("e_u32".into(), FieldType::U32),
404 ("f_i16".into(), FieldType::I16),
405 ("g_u16".into(), FieldType::U16),
406 ("h_i8".into(), FieldType::I8),
407 ("i_u8".into(), FieldType::U8),
408 ("j_bool".into(), FieldType::Bool),
409 ]);
410
411 // All 8-byte fields first (no padding between them or after header)
412 assert_eq!(layout.fields[0].offset, 8); // f64 @ 8
413 assert_eq!(layout.fields[1].offset, 16); // i64 @ 16
414 assert_eq!(layout.fields[2].offset, 24); // ptr @ 24
415
416 // 4-byte fields (naturally aligned after ptr ends at 32)
417 assert_eq!(layout.fields[3].offset, 32); // i32 @ 32
418 assert_eq!(layout.fields[4].offset, 36); // u32 @ 36
419
420 // 2-byte fields (naturally aligned after u32 ends at 40)
421 assert_eq!(layout.fields[5].offset, 40); // i16 @ 40
422 assert_eq!(layout.fields[6].offset, 42); // u16 @ 42
423
424 // 1-byte fields (no alignment needed)
425 assert_eq!(layout.fields[7].offset, 44); // i8 @ 44
426 assert_eq!(layout.fields[8].offset, 45); // u8 @ 45
427 assert_eq!(layout.fields[9].offset, 46); // bool @ 46
428
429 // Total: 47, padded to 8 = 48
430 assert_eq!(layout.total_size, 48);
431 assert_eq!(layout.alignment(), 8);
432 }
433
434 // -----------------------------------------------------------------------
435 // Padding between small and large fields
436 // -----------------------------------------------------------------------
437
438 #[test]
439 fn test_i16_then_i64_padding() {
440 // type T { a: i16, b: i64 }
441 let layout = compute_struct_layout("T", &[
442 ("a".into(), FieldType::I16),
443 ("b".into(), FieldType::I64),
444 ]);
445
446 // a: i16 at offset 8 (2 bytes)
447 assert_eq!(layout.fields[0].offset, 8);
448 // b: i64 needs 8-byte alignment. cursor=10, aligned to 8 → 16
449 assert_eq!(layout.fields[1].offset, 16);
450 // Total: 16 + 8 = 24, already aligned to 8
451 assert_eq!(layout.total_size, 24);
452 }
453
454 #[test]
455 fn test_bool_i32_bool_padding() {
456 // type T { a: bool, b: i32, c: bool }
457 let layout = compute_struct_layout("T", &[
458 ("a".into(), FieldType::Bool),
459 ("b".into(), FieldType::I32),
460 ("c".into(), FieldType::Bool),
461 ]);
462
463 // a: bool at offset 8
464 assert_eq!(layout.fields[0].offset, 8);
465 // b: i32 needs 4-byte alignment. cursor=9, aligned to 4 → 12
466 assert_eq!(layout.fields[1].offset, 12);
467 // c: bool at offset 16 (12+4=16, 1-byte aligned)
468 assert_eq!(layout.fields[2].offset, 16);
469 // Total: 17, padded to max_align=8 → 24
470 assert_eq!(layout.total_size, 24);
471 }
472
473 // -----------------------------------------------------------------------
474 // Pointer fields
475 // -----------------------------------------------------------------------
476
477 #[test]
478 fn test_struct_with_pointer_fields() {
479 // type Node { value: i32, next: ptr, prev: ptr }
480 let layout = compute_struct_layout("Node", &[
481 ("value".into(), FieldType::I32),
482 ("next".into(), FieldType::Ptr),
483 ("prev".into(), FieldType::Ptr),
484 ]);
485
486 // value: i32 at offset 8
487 assert_eq!(layout.fields[0].offset, 8);
488 assert_eq!(layout.fields[0].size, 4);
489
490 // next: ptr needs 8-byte alignment. cursor=12, aligned to 8 → 16
491 assert_eq!(layout.fields[1].offset, 16);
492 assert_eq!(layout.fields[1].size, 8);
493
494 // prev: ptr at offset 24
495 assert_eq!(layout.fields[2].offset, 24);
496 assert_eq!(layout.fields[2].size, 8);
497
498 // Total: 32, already aligned to 8
499 assert_eq!(layout.total_size, 32);
500 }
501
502 // -----------------------------------------------------------------------
503 // field() lookup
504 // -----------------------------------------------------------------------
505
506 #[test]
507 fn test_field_lookup_by_name() {
508 let layout = compute_struct_layout("Point", &[
509 ("x".into(), FieldType::F64),
510 ("y".into(), FieldType::F64),
511 ]);
512
513 let x = layout.field("x").expect("field 'x' should exist");
514 assert_eq!(x.offset, 8);
515 assert_eq!(x.field_type, FieldType::F64);
516
517 let y = layout.field("y").expect("field 'y' should exist");
518 assert_eq!(y.offset, 16);
519 assert_eq!(y.field_type, FieldType::F64);
520
521 assert!(layout.field("z").is_none());
522 }
523
524 // -----------------------------------------------------------------------
525 // Worst-case padding scenario
526 // -----------------------------------------------------------------------
527
528 #[test]
529 fn test_worst_case_padding() {
530 // Deliberately adversarial ordering: small, large, small, large
531 // type Padded { a: u8, b: f64, c: u8, d: f64 }
532 let layout = compute_struct_layout("Padded", &[
533 ("a".into(), FieldType::U8),
534 ("b".into(), FieldType::F64),
535 ("c".into(), FieldType::U8),
536 ("d".into(), FieldType::F64),
537 ]);
538
539 // a: u8 at offset 8
540 assert_eq!(layout.fields[0].offset, 8);
541 // b: f64 needs 8-byte alignment. cursor=9 → 16
542 assert_eq!(layout.fields[1].offset, 16);
543 // c: u8 at offset 24
544 assert_eq!(layout.fields[2].offset, 24);
545 // d: f64 needs 8-byte alignment. cursor=25 → 32
546 assert_eq!(layout.fields[3].offset, 32);
547 // Total: 32 + 8 = 40, aligned to 8 = 40
548 assert_eq!(layout.total_size, 40);
549 }
550
551 // -----------------------------------------------------------------------
552 // Compile-time constant offsets — verifies offsets are deterministic
553 // -----------------------------------------------------------------------
554
555 #[test]
556 fn test_layout_is_deterministic() {
557 let fields: Vec<(String, FieldType)> = vec![
558 ("a".into(), FieldType::I32),
559 ("b".into(), FieldType::Bool),
560 ("c".into(), FieldType::F64),
561 ];
562
563 let layout1 = compute_struct_layout("T", &fields);
564 let layout2 = compute_struct_layout("T", &fields);
565
566 assert_eq!(layout1, layout2);
567 }
568
569 // -----------------------------------------------------------------------
570 // Only 16-bit fields
571 // -----------------------------------------------------------------------
572
573 #[test]
574 fn test_only_16bit_fields() {
575 let layout = compute_struct_layout("Shorts", &[
576 ("a".into(), FieldType::I16),
577 ("b".into(), FieldType::U16),
578 ("c".into(), FieldType::I16),
579 ]);
580
581 assert_eq!(layout.fields[0].offset, 8); // i16 at 8
582 assert_eq!(layout.fields[1].offset, 10); // u16 at 10
583 assert_eq!(layout.fields[2].offset, 12); // i16 at 12
584
585 // Total: 14, padded to max_align=8 → 16
586 assert_eq!(layout.total_size, 16);
587 }
588
589 // -----------------------------------------------------------------------
590 // V2_HEADER constants
591 // -----------------------------------------------------------------------
592
593 #[test]
594 fn test_header_constants() {
595 assert_eq!(V2_HEADER_SIZE, 8);
596 assert_eq!(V2_HEADER_ALIGN, 8);
597 }
598}