Skip to main content

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}