Skip to main content

padlock_core/analysis/
locality.rs

1// padlock-core/src/analysis/locality.rs
2
3use crate::ir::{AccessPattern, Field, StructLayout};
4
5pub struct FieldLocality<'a> {
6    pub field: &'a Field,
7    pub is_hot: bool,
8}
9
10fn is_hot(f: &Field) -> bool {
11    matches!(
12        f.access,
13        AccessPattern::ReadMostly | AccessPattern::Concurrent { .. }
14    )
15}
16
17/// Classify every field as hot or cold.
18pub fn classify_fields(layout: &StructLayout) -> Vec<FieldLocality<'_>> {
19    layout
20        .fields
21        .iter()
22        .map(|f| FieldLocality {
23            field: f,
24            is_hot: is_hot(f),
25        })
26        .collect()
27}
28
29/// Returns `true` if hot and cold fields are interleaved (a locality problem).
30/// If all fields are hot or all are cold the layout is fine.
31pub fn has_locality_issue(layout: &StructLayout) -> bool {
32    let classified = classify_fields(layout);
33    let has_hot = classified.iter().any(|c| c.is_hot);
34    let has_cold = classified.iter().any(|c| !c.is_hot);
35    if !has_hot || !has_cold {
36        return false;
37    }
38    // Scan for a cold→hot or hot→cold→hot transition (interleaving).
39    let mut saw_cold_after_hot = false;
40    let mut last_was_hot = false;
41    for c in &classified {
42        if c.is_hot {
43            if saw_cold_after_hot {
44                return true; // hot field appears after a cold field that followed a hot field
45            }
46            last_was_hot = true;
47        } else {
48            if last_was_hot {
49                saw_cold_after_hot = true;
50            }
51        }
52    }
53    false
54}
55
56/// Split fields into (hot_names, cold_names) preserving original order.
57pub fn partition_hot_cold(layout: &StructLayout) -> (Vec<String>, Vec<String>) {
58    let mut hot = Vec::new();
59    let mut cold = Vec::new();
60    for f in &layout.fields {
61        if is_hot(f) {
62            hot.push(f.name.clone());
63        } else {
64            cold.push(f.name.clone());
65        }
66    }
67    (hot, cold)
68}
69
70// ── tests ─────────────────────────────────────────────────────────────────────
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::arch::X86_64_SYSV;
76    use crate::ir::{Field, StructLayout, TypeInfo};
77
78    fn field(name: &str, offset: usize, access: AccessPattern) -> Field {
79        Field {
80            name: name.into(),
81            ty: TypeInfo::Primitive {
82                name: "u64".into(),
83                size: 8,
84                align: 8,
85            },
86            offset,
87            size: 8,
88            align: 8,
89            source_file: None,
90            source_line: None,
91            access,
92        }
93    }
94
95    fn layout(fields: Vec<Field>) -> StructLayout {
96        StructLayout {
97            name: "T".into(),
98            total_size: fields.len() * 8,
99            align: 8,
100            fields,
101            source_file: None,
102            source_line: None,
103            arch: &X86_64_SYSV,
104            is_packed: false,
105            is_union: false,
106            is_repr_rust: false,
107            suppressed_findings: Vec::new(),
108        }
109    }
110
111    #[test]
112    fn interleaved_hot_cold_is_issue() {
113        // hot cold hot — locality issue
114        let l = layout(vec![
115            field("a", 0, AccessPattern::ReadMostly),
116            field("b", 8, AccessPattern::Unknown),
117            field("c", 16, AccessPattern::ReadMostly),
118        ]);
119        assert!(has_locality_issue(&l));
120    }
121
122    #[test]
123    fn hot_first_then_cold_is_fine() {
124        let l = layout(vec![
125            field("a", 0, AccessPattern::ReadMostly),
126            field("b", 8, AccessPattern::ReadMostly),
127            field("c", 16, AccessPattern::Unknown),
128            field("d", 24, AccessPattern::Unknown),
129        ]);
130        assert!(!has_locality_issue(&l));
131    }
132
133    #[test]
134    fn all_unknown_no_issue() {
135        let l = layout(vec![
136            field("a", 0, AccessPattern::Unknown),
137            field("b", 8, AccessPattern::Unknown),
138        ]);
139        assert!(!has_locality_issue(&l));
140    }
141
142    #[test]
143    fn partition_separates_correctly() {
144        let l = layout(vec![
145            field("a", 0, AccessPattern::ReadMostly),
146            field("b", 8, AccessPattern::Unknown),
147            field(
148                "c",
149                16,
150                AccessPattern::Concurrent {
151                    guard: None,
152                    is_atomic: true,
153                    is_annotated: false,
154                },
155            ),
156        ]);
157        let (hot, cold) = partition_hot_cold(&l);
158        assert_eq!(hot, vec!["a", "c"]);
159        assert_eq!(cold, vec!["b"]);
160    }
161}