layout_audit/analysis/
optimize.rs

1//! Field reordering optimization for struct layouts.
2
3use crate::types::{MemberLayout, StructLayout};
4use serde::Serialize;
5use std::collections::HashSet;
6
7/// Result of optimizing a struct layout.
8#[derive(Debug, Clone, Serialize)]
9pub struct OptimizedLayout {
10    pub name: String,
11    pub original_size: u64,
12    pub optimized_size: u64,
13    pub savings_bytes: u64,
14    pub savings_percent: f64,
15    pub struct_alignment: u64,
16    pub original_members: Vec<OptimizedMember>,
17    pub optimized_members: Vec<OptimizedMember>,
18    /// Members that could not be optimized (missing size/offset).
19    #[serde(skip_serializing_if = "Vec::is_empty")]
20    pub skipped_members: Vec<String>,
21    /// True if layout contains bitfields that were kept together.
22    pub has_bitfields: bool,
23}
24
25/// Member with computed offset and alignment.
26#[derive(Debug, Clone, Serialize)]
27pub struct OptimizedMember {
28    pub name: String,
29    pub type_name: String,
30    pub offset: u64,
31    pub size: u64,
32    pub alignment: u64,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub bit_offset: Option<u64>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub bit_size: Option<u64>,
37}
38
39/// Infer alignment from size using standard C ABI rules.
40/// Returns alignment as power of 2, capped at max_align.
41pub fn infer_alignment(size: u64, max_align: u64) -> u64 {
42    if size == 0 {
43        return 1;
44    }
45    // For primitives: alignment = min(size rounded to power of 2, max_align)
46    let natural_align = size.next_power_of_two();
47    natural_align.min(max_align).max(1)
48}
49
50/// Align value up to alignment boundary.
51/// Returns value unchanged if alignment <= 1.
52/// For values near u64::MAX where alignment would overflow, returns u64::MAX.
53fn align_up(value: u64, alignment: u64) -> u64 {
54    if alignment <= 1 {
55        return value;
56    }
57    // Works for any positive alignment (not only powers of two).
58    // Use checked arithmetic to detect overflow near u64::MAX.
59    match value.checked_add(alignment - 1) {
60        Some(sum) => (sum / alignment) * alignment,
61        // Overflow: value is near u64::MAX and can't be aligned up safely.
62        // Return u64::MAX as a safe upper bound (real structs won't hit this).
63        None => u64::MAX,
64    }
65}
66
67/// A sortable unit for optimization - either a single member or a bitfield group.
68#[derive(Clone)]
69struct SortableUnit {
70    members: Vec<OptimizedMember>,
71    total_size: u64,
72    alignment: u64,
73}
74
75/// Group bitfield members that share storage units.
76/// Returns indices of members belonging to each bitfield group.
77/// Note: Members in bitfield groups may still be filtered out during optimization
78/// if they have missing size/offset. Callers must track converted indices separately.
79fn find_bitfield_groups(members: &[MemberLayout]) -> Vec<Vec<usize>> {
80    let mut groups: Vec<Vec<usize>> = Vec::new();
81    let mut current_group: Vec<usize> = Vec::new();
82    let mut current_offset: Option<u64> = None;
83
84    for (idx, member) in members.iter().enumerate() {
85        if member.bit_size.is_some() {
86            let base_offset = member.offset;
87            // Only group bitfields with known, matching offsets.
88            // None == None should NOT match (can't verify they share storage).
89            let same_storage = match (current_offset, base_offset) {
90                (Some(curr), Some(base)) => curr == base,
91                _ => false,
92            };
93            if same_storage && !current_group.is_empty() {
94                // Same storage unit
95                current_group.push(idx);
96            } else {
97                // New storage unit
98                if !current_group.is_empty() {
99                    groups.push(std::mem::take(&mut current_group));
100                }
101                current_group.push(idx);
102                current_offset = base_offset;
103            }
104        } else if !current_group.is_empty() {
105            // Non-bitfield breaks the group
106            groups.push(std::mem::take(&mut current_group));
107            current_offset = None;
108        }
109    }
110
111    if !current_group.is_empty() {
112        groups.push(current_group);
113    }
114
115    groups
116}
117
118/// Optimize a struct layout by reordering fields to minimize padding.
119/// Uses greedy bin-packing: sort by alignment desc, then size desc.
120pub fn optimize_layout(layout: &StructLayout, max_align: u64) -> OptimizedLayout {
121    let max_align = max_align.max(1);
122    // If struct alignment is known, use it; otherwise infer from member alignments.
123    // Exclude ZSTs (size=0) since they don't affect struct alignment.
124    let inferred_alignment = layout
125        .members
126        .iter()
127        .filter_map(|m| m.size)
128        .filter(|&s| s > 0)
129        .map(|s| infer_alignment(s, max_align))
130        .max()
131        .unwrap_or(1);
132
133    let struct_alignment = layout.alignment.unwrap_or(inferred_alignment).min(max_align);
134
135    // Find bitfield groups
136    let bitfield_groups = find_bitfield_groups(&layout.members);
137    let bitfield_indices: HashSet<usize> = bitfield_groups.iter().flatten().copied().collect();
138    let has_bitfields = !bitfield_groups.is_empty();
139
140    // Build original members list with inferred alignment
141    let mut original_members: Vec<OptimizedMember> = Vec::new();
142    let mut skipped_members: Vec<String> = Vec::new();
143
144    for member in &layout.members {
145        let Some(size) = member.size else {
146            skipped_members.push(member.name.clone());
147            continue;
148        };
149        let Some(offset) = member.offset else {
150            skipped_members.push(member.name.clone());
151            continue;
152        };
153        // Skip zero-size types (ZSTs don't affect layout)
154        if size == 0 {
155            skipped_members.push(format!("{} (zero-size)", member.name));
156            continue;
157        }
158
159        let alignment = infer_alignment(size, max_align);
160
161        original_members.push(OptimizedMember {
162            name: member.name.clone(),
163            type_name: member.type_name.clone(),
164            offset,
165            size,
166            alignment,
167            bit_offset: member.bit_offset,
168            bit_size: member.bit_size,
169        });
170    }
171
172    // Build sortable units
173    let mut units: Vec<SortableUnit> = Vec::new();
174    let mut processed_indices: HashSet<usize> = HashSet::new();
175
176    // Track which bitfield indices were successfully converted to OptimizedMember.
177    // This allows us to verify no bitfield member is silently lost.
178    let mut converted_bitfield_indices: HashSet<usize> = HashSet::new();
179
180    // Add bitfield groups as units
181    for group in &bitfield_groups {
182        if group.is_empty() {
183            continue;
184        }
185
186        // Track which indices in this group were successfully converted
187        let mut group_converted: Vec<usize> = Vec::new();
188        let group_members: Vec<OptimizedMember> = group
189            .iter()
190            .filter_map(|&idx| {
191                layout.members.get(idx).and_then(|lm| {
192                    original_members.iter().find(|m| m.name == lm.name).cloned().inspect(|_| {
193                        group_converted.push(idx);
194                    })
195                })
196            })
197            .collect();
198
199        if group_members.is_empty() {
200            continue;
201        }
202
203        // Bitfield group size = storage unit size (from first member)
204        // Skip groups where we can't determine size reliably.
205        let Some(total_size) = group_members.first().map(|m| m.size) else {
206            continue;
207        };
208        let alignment = group_members.iter().map(|m| m.alignment).max().unwrap_or(1);
209
210        // Mark only successfully converted indices as processed
211        for idx in &group_converted {
212            processed_indices.insert(*idx);
213            converted_bitfield_indices.insert(*idx);
214        }
215
216        units.push(SortableUnit { members: group_members, total_size, alignment });
217    }
218
219    // Verify all bitfield indices are accounted for (either converted or in skipped_members).
220    // This defensive check prevents silent data loss from edge cases.
221    // Collect names to add first to avoid borrow conflicts.
222    let missing_bitfield_names: Vec<String> = {
223        let skipped_names: HashSet<&str> = skipped_members.iter().map(|s| s.as_str()).collect();
224        bitfield_indices
225            .iter()
226            .filter(|&&idx| !converted_bitfield_indices.contains(&idx))
227            .filter_map(|&idx| layout.members.get(idx))
228            .filter(|member| {
229                // Check if already in skipped_members (may have "(zero-size)" suffix)
230                !skipped_names.contains(member.name.as_str())
231                    && !skipped_members.iter().any(|s| s.starts_with(&member.name))
232            })
233            .map(|member| format!("{} (bitfield, missing info)", member.name))
234            .collect()
235    };
236    skipped_members.extend(missing_bitfield_names);
237
238    // Add non-bitfield members as single-member units
239    for (idx, member) in layout.members.iter().enumerate() {
240        if processed_indices.contains(&idx) || bitfield_indices.contains(&idx) {
241            continue;
242        }
243
244        if let Some(opt_member) = original_members.iter().find(|m| m.name == member.name) {
245            units.push(SortableUnit {
246                members: vec![opt_member.clone()],
247                total_size: opt_member.size,
248                alignment: opt_member.alignment,
249            });
250        }
251    }
252
253    // Sort: largest alignment first, then largest size
254    units.sort_by(|a, b| {
255        b.alignment.cmp(&a.alignment).then_with(|| b.total_size.cmp(&a.total_size))
256    });
257
258    // Place members greedily
259    let mut optimized_members: Vec<OptimizedMember> = Vec::new();
260    let mut current_offset: u64 = 0;
261
262    for unit in units {
263        // Align to unit's alignment requirement
264        let aligned_offset = align_up(current_offset, unit.alignment);
265
266        for mut member in unit.members {
267            member.offset = aligned_offset;
268            // Clear bit_offset after reordering - the original value was relative to
269            // the original layout and is no longer valid. Keep bit_size for reference.
270            if member.bit_size.is_some() {
271                member.bit_offset = None;
272            }
273            optimized_members.push(member);
274        }
275
276        // Use saturating_add to prevent overflow near u64::MAX
277        current_offset = aligned_offset.saturating_add(unit.total_size);
278    }
279
280    // Add tail padding to reach struct alignment
281    let optimized_size = align_up(current_offset, struct_alignment);
282
283    let savings_bytes = layout.size.saturating_sub(optimized_size);
284    let savings_percent =
285        if layout.size > 0 { (savings_bytes as f64 / layout.size as f64) * 100.0 } else { 0.0 };
286
287    OptimizedLayout {
288        name: layout.name.clone(),
289        original_size: layout.size,
290        optimized_size,
291        savings_bytes,
292        savings_percent,
293        struct_alignment,
294        original_members,
295        optimized_members,
296        skipped_members,
297        has_bitfields,
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_infer_alignment() {
307        assert_eq!(infer_alignment(1, 8), 1); // char
308        assert_eq!(infer_alignment(2, 8), 2); // short
309        assert_eq!(infer_alignment(4, 8), 4); // int
310        assert_eq!(infer_alignment(8, 8), 8); // long/pointer
311        assert_eq!(infer_alignment(16, 8), 8); // capped at max_align
312        assert_eq!(infer_alignment(3, 8), 4); // rounds up to power of 2
313        assert_eq!(infer_alignment(0, 8), 1); // ZST
314    }
315
316    #[test]
317    fn test_align_up() {
318        // Basic cases
319        assert_eq!(align_up(0, 4), 0);
320        assert_eq!(align_up(1, 4), 4);
321        assert_eq!(align_up(4, 4), 4);
322        assert_eq!(align_up(5, 4), 8);
323        assert_eq!(align_up(7, 8), 8);
324
325        // Non-power-of-two alignments
326        assert_eq!(align_up(4, 3), 6);
327        assert_eq!(align_up(6, 3), 6);
328        assert_eq!(align_up(10, 12), 12);
329        assert_eq!(align_up(24, 12), 24);
330
331        // Edge case: alignment = 0 (returns value unchanged)
332        assert_eq!(align_up(10, 0), 10);
333
334        // Edge case: alignment = 1 (returns value unchanged)
335        assert_eq!(align_up(5, 1), 5);
336        assert_eq!(align_up(0, 1), 0);
337
338        // Overflow protection: value near u64::MAX
339        assert_eq!(align_up(u64::MAX - 5, 16), u64::MAX);
340        assert_eq!(align_up(u64::MAX, 8), u64::MAX);
341    }
342
343    #[test]
344    fn test_optimize_padded_struct() {
345        // struct { char a; int b; char c; } = 12 bytes with padding
346        // optimal: struct { int b; char a; char c; } = 8 bytes
347        let mut layout = StructLayout::new("Test".to_string(), 12, Some(4));
348        layout.members = vec![
349            MemberLayout::new("a".to_string(), "char".to_string(), Some(0), Some(1)),
350            MemberLayout::new("b".to_string(), "int".to_string(), Some(4), Some(4)),
351            MemberLayout::new("c".to_string(), "char".to_string(), Some(8), Some(1)),
352        ];
353
354        let result = optimize_layout(&layout, 8);
355
356        assert_eq!(result.original_size, 12);
357        assert_eq!(result.optimized_size, 8);
358        assert_eq!(result.savings_bytes, 4);
359
360        // First member should be the int (largest alignment)
361        assert_eq!(result.optimized_members[0].name, "b");
362        assert_eq!(result.optimized_members[0].offset, 0);
363    }
364
365    #[test]
366    fn test_already_optimal() {
367        // struct { int a; int b; } = 8 bytes, already optimal
368        let mut layout = StructLayout::new("Test".to_string(), 8, Some(4));
369        layout.members = vec![
370            MemberLayout::new("a".to_string(), "int".to_string(), Some(0), Some(4)),
371            MemberLayout::new("b".to_string(), "int".to_string(), Some(4), Some(4)),
372        ];
373
374        let result = optimize_layout(&layout, 8);
375
376        assert_eq!(result.original_size, 8);
377        assert_eq!(result.optimized_size, 8);
378        assert_eq!(result.savings_bytes, 0);
379    }
380
381    #[test]
382    fn test_skipped_members() {
383        let mut layout = StructLayout::new("Test".to_string(), 16, Some(8));
384        layout.members = vec![
385            MemberLayout::new("a".to_string(), "int".to_string(), Some(0), Some(4)),
386            MemberLayout::new("b".to_string(), "unknown".to_string(), None, None), // missing info
387        ];
388
389        let result = optimize_layout(&layout, 8);
390
391        assert_eq!(result.skipped_members, vec!["b"]);
392    }
393
394    #[test]
395    fn test_max_align_zero_is_safely_clamped() {
396        let mut layout = StructLayout::new("Test".to_string(), 12, Some(4));
397        layout.members = vec![
398            MemberLayout::new("a".to_string(), "char".to_string(), Some(0), Some(1)),
399            MemberLayout::new("b".to_string(), "int".to_string(), Some(4), Some(4)),
400        ];
401
402        let result = optimize_layout(&layout, 0);
403        assert!(result.struct_alignment >= 1);
404        assert!(result.optimized_size > 0);
405    }
406
407    #[test]
408    fn test_bitfield_with_missing_metadata_not_lost() {
409        // Test that bitfield members with missing metadata are tracked in skipped_members
410        let mut layout = StructLayout::new("Test".to_string(), 8, Some(4));
411
412        // Create a bitfield member with valid info
413        let mut valid_bitfield =
414            MemberLayout::new("flags".to_string(), "unsigned int".to_string(), Some(0), Some(4));
415        valid_bitfield.bit_size = Some(3);
416        valid_bitfield.bit_offset = Some(0);
417
418        // Create a bitfield member with missing metadata (same offset = same storage unit)
419        let mut missing_bitfield =
420            MemberLayout::new("status".to_string(), "unsigned int".to_string(), Some(0), None);
421        missing_bitfield.bit_size = Some(2);
422
423        layout.members = vec![valid_bitfield, missing_bitfield];
424
425        let result = optimize_layout(&layout, 8);
426
427        // The valid bitfield should be optimized
428        assert!(result.optimized_members.iter().any(|m| m.name == "flags"));
429
430        // The missing bitfield should be in skipped_members (not silently lost)
431        assert!(
432            result.skipped_members.iter().any(|s| s.contains("status")),
433            "Bitfield 'status' with missing metadata should be in skipped_members, got: {:?}",
434            result.skipped_members
435        );
436    }
437}