Skip to main content

padlock_source/frontends/
rust.rs

1// padlock-source/src/frontends/rust.rs
2//
3// Extracts struct layouts from Rust source using syn + the Visit API.
4// Sizes are approximated from type names using the target arch config.
5// Only repr(C) / repr(packed) / plain structs are handled; generics are opaque.
6
7use padlock_core::arch::ArchConfig;
8use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
9use quote::ToTokens;
10use syn::{Fields, ItemEnum, ItemStruct, Type, visit::Visit};
11
12// ── attribute guard extraction ────────────────────────────────────────────────
13
14/// Extract a lock guard name from field attributes.
15///
16/// Recognised forms:
17/// - `#[lock_protected_by = "mu"]`
18/// - `#[protected_by = "mu"]`
19/// - `#[guarded_by("mu")]` or `#[guarded_by(mu)]`
20/// - `#[pt_guarded_by("mu")]` or `#[pt_guarded_by(mu)]` (pointer variant)
21pub fn extract_guard_from_attrs(attrs: &[syn::Attribute]) -> Option<String> {
22    for attr in attrs {
23        let path = attr.path();
24        // Name-value form: #[lock_protected_by = "mu"] / #[protected_by = "mu"]
25        if (path.is_ident("lock_protected_by") || path.is_ident("protected_by"))
26            && let syn::Meta::NameValue(nv) = &attr.meta
27            && let syn::Expr::Lit(syn::ExprLit {
28                lit: syn::Lit::Str(s),
29                ..
30            }) = &nv.value
31        {
32            return Some(s.value());
33        }
34        // List form: #[guarded_by("mu")] / #[guarded_by(mu)] / #[pt_guarded_by(...)]
35        if path.is_ident("guarded_by") || path.is_ident("pt_guarded_by") {
36            // Try string literal first
37            if let Ok(s) = attr.parse_args::<syn::LitStr>() {
38                return Some(s.value());
39            }
40            // Fall back to bare identifier
41            if let Ok(id) = attr.parse_args::<syn::Ident>() {
42                return Some(id.to_string());
43            }
44        }
45    }
46    None
47}
48
49// ── type resolution ───────────────────────────────────────────────────────────
50
51fn rust_type_size_align(ty: &Type, arch: &'static ArchConfig) -> (usize, usize, TypeInfo) {
52    match ty {
53        Type::Path(tp) => {
54            let name = tp
55                .path
56                .segments
57                .last()
58                .map(|s| s.ident.to_string())
59                .unwrap_or_default();
60            let (size, align) = primitive_size_align(&name, arch);
61            (size, align, TypeInfo::Primitive { name, size, align })
62        }
63        Type::Ptr(_) | Type::Reference(_) => {
64            let s = arch.pointer_size;
65            (s, s, TypeInfo::Pointer { size: s, align: s })
66        }
67        Type::Array(arr) => {
68            let (elem_size, elem_align, elem_ty) = rust_type_size_align(&arr.elem, arch);
69            let count = array_len_from_expr(&arr.len);
70            let size = elem_size * count;
71            (
72                size,
73                elem_align,
74                TypeInfo::Array {
75                    element: Box::new(elem_ty),
76                    count,
77                    size,
78                    align: elem_align,
79                },
80            )
81        }
82        _ => {
83            let s = arch.pointer_size;
84            (
85                s,
86                s,
87                TypeInfo::Opaque {
88                    name: "(unknown)".into(),
89                    size: s,
90                    align: s,
91                },
92            )
93        }
94    }
95}
96
97fn primitive_size_align(name: &str, arch: &'static ArchConfig) -> (usize, usize) {
98    let ps = arch.pointer_size;
99    match name {
100        // ── language primitives ───────────────────────────────────────────────
101        "bool" | "u8" | "i8" => (1, 1),
102        "u16" | "i16" | "f16" => (2, 2),
103        "u32" | "i32" | "f32" => (4, 4),
104        "u64" | "i64" | "f64" => (8, 8),
105        "u128" | "i128" | "f128" => (16, 16),
106        "usize" | "isize" => (ps, ps),
107        "char" => (4, 4), // Rust char is a Unicode scalar (4 bytes)
108
109        // NonZero integer types — same size/align as the underlying integer.
110        // The niche optimisation means Option<NonZeroU8> == 1 byte, but the
111        // struct field itself is identical in size to the plain integer.
112        "NonZeroU8" | "NonZeroI8" => (1, 1),
113        "NonZeroU16" | "NonZeroI16" => (2, 2),
114        "NonZeroU32" | "NonZeroI32" => (4, 4),
115        "NonZeroU64" | "NonZeroI64" => (8, 8),
116        "NonZeroU128" | "NonZeroI128" => (16, 16),
117        "NonZeroUsize" | "NonZeroIsize" => (ps, ps),
118
119        // Wrapping<T>, Saturating<T> — transparent newtype over T.
120        // The generic arg has already been stripped, so we get the inner
121        // primitive name here; if the stripping didn't happen these fall
122        // through to pointer-size, which is acceptable.
123        "Wrapping" | "Saturating" => (ps, ps),
124
125        // MaybeUninit<T> and UnsafeCell<T> are transparent newtypes —
126        // same size as T. Without knowing T we approximate as pointer-size,
127        // which is correct for the common case of wrapping a pointer-sized value.
128        "MaybeUninit" | "UnsafeCell" => (ps, ps),
129
130        // ── std atomics ───────────────────────────────────────────────────────
131        "AtomicBool" | "AtomicU8" | "AtomicI8" => (1, 1),
132        "AtomicU16" | "AtomicI16" => (2, 2),
133        "AtomicU32" | "AtomicI32" => (4, 4),
134        "AtomicU64" | "AtomicI64" => (8, 8),
135        "AtomicUsize" | "AtomicIsize" | "AtomicPtr" => (ps, ps),
136
137        // ── heap-allocated collections: ptr + len + cap (3 words) ────────────
138        // Size is independent of the element type T (generic arg already stripped).
139        "Vec" | "String" | "OsString" | "CString" | "PathBuf" => (3 * ps, ps),
140        "VecDeque" | "LinkedList" | "BinaryHeap" => (3 * ps, ps),
141        "HashMap" | "HashSet" | "BTreeMap" | "BTreeSet" => (3 * ps, ps),
142
143        // ── single-pointer smart pointers ─────────────────────────────────────
144        "Box" | "Rc" | "Arc" | "Weak" | "NonNull" | "Cell" => (ps, ps),
145
146        // ── interior-mutability / sync wrappers ───────────────────────────────
147        // Size depends on T but pointer-size is a reasonable approximation for
148        // display purposes; use binary analysis for precise results.
149        "RefCell" | "Mutex" | "RwLock" => (ps, ps),
150
151        // ── channels ─────────────────────────────────────────────────────────
152        "Sender" | "Receiver" | "SyncSender" => (ps, ps),
153
154        // ── zero-sized types ──────────────────────────────────────────────────
155        "PhantomData" | "PhantomPinned" => (0, 1),
156
157        // ── common fixed-size stdlib types ────────────────────────────────────
158        // Duration: u64 secs (8B) + u32 nanos (4B) → 12B + 4B trailing = 16B
159        "Duration" => (16, 8),
160        "Instant" | "SystemTime" => (16, 8),
161
162        // ── Pin<T> wraps T, pointer-size approximation ────────────────────────
163        "Pin" => (ps, ps),
164
165        // ── x86 SSE / AVX / AVX-512 SIMD types ───────────────────────────────
166        "__m64" => (8, 8),
167        "__m128" | "__m128d" | "__m128i" => (16, 16),
168        "__m256" | "__m256d" | "__m256i" => (32, 32),
169        "__m512" | "__m512d" | "__m512i" => (64, 64),
170
171        // ── Rust portable SIMD / packed_simd types ────────────────────────────
172        "f32x4" | "i32x4" | "u32x4" => (16, 16),
173        "f64x2" | "i64x2" | "u64x2" => (16, 16),
174        "f32x8" | "i32x8" | "u32x8" => (32, 32),
175        "f64x4" | "i64x4" | "u64x4" => (32, 32),
176        "f32x16" | "i32x16" | "u32x16" => (64, 64),
177
178        // ── unknown / third-party / generic type params (T, E, …) ────────────
179        _ => (ps, ps),
180    }
181}
182
183fn array_len_from_expr(expr: &syn::Expr) -> usize {
184    if let syn::Expr::Lit(syn::ExprLit {
185        lit: syn::Lit::Int(n),
186        ..
187    }) = expr
188    {
189        n.base10_parse::<usize>().unwrap_or(0)
190    } else {
191        0
192    }
193}
194
195// ── struct repr detection ─────────────────────────────────────────────────────
196
197fn is_packed(attrs: &[syn::Attribute]) -> bool {
198    attrs
199        .iter()
200        .any(|a| a.path().is_ident("repr") && a.to_token_stream().to_string().contains("packed"))
201}
202
203/// Returns `true` when the struct has no repr annotation that fixes the layout
204/// (`repr(C)`, `repr(packed)`, `repr(transparent)`).  A struct with only
205/// `repr(align(N))` still has an unspecified field order — the compiler may
206/// reorder fields freely — so it counts as `repr(Rust)` for warning purposes.
207fn is_repr_rust(attrs: &[syn::Attribute]) -> bool {
208    !attrs.iter().any(|a| {
209        if !a.path().is_ident("repr") {
210            return false;
211        }
212        let ts = a.to_token_stream().to_string();
213        ts.contains('C') || ts.contains("packed") || ts.contains("transparent")
214    })
215}
216
217/// Extract the alignment from `#[repr(align(N))]`. Returns `None` if not present.
218fn repr_align(attrs: &[syn::Attribute]) -> Option<usize> {
219    for attr in attrs {
220        if !attr.path().is_ident("repr") {
221            continue;
222        }
223        let ts = attr.to_token_stream().to_string();
224        // Look for `align ( N )` in the token stream string.
225        // The tokeniser adds spaces: "repr (align (64))" etc.
226        if let Some(start) = ts.find("align") {
227            let after = ts[start..].trim_start_matches("align").trim_start();
228            if after.starts_with('(') {
229                let inner = after.trim_start_matches('(');
230                let num_str: String = inner.chars().take_while(|c| c.is_ascii_digit()).collect();
231                if let Ok(n) = num_str.parse::<usize>()
232                    && n > 0
233                    && n.is_power_of_two()
234                {
235                    return Some(n);
236                }
237            }
238        }
239    }
240    None
241}
242
243fn simulate_rust_layout(
244    name: String,
245    fields: &[(String, Type)],
246    packed: bool,
247    forced_align: Option<usize>,
248    arch: &'static ArchConfig,
249) -> StructLayout {
250    let mut offset = 0usize;
251    let mut struct_align = 1usize;
252    let mut out_fields: Vec<Field> = Vec::new();
253
254    for (fname, ty) in fields {
255        let (size, align, type_info) = rust_type_size_align(ty, arch);
256        let effective_align = if packed { 1 } else { align };
257
258        if effective_align > 0 {
259            offset = offset.next_multiple_of(effective_align);
260        }
261        struct_align = struct_align.max(effective_align);
262
263        out_fields.push(Field {
264            name: fname.clone(),
265            ty: type_info,
266            offset,
267            size,
268            align: effective_align,
269            source_file: None,
270            source_line: None,
271            access: AccessPattern::Unknown,
272        });
273        offset += size;
274    }
275
276    // Apply repr(align(N)): raise minimum alignment and add trailing padding.
277    if let Some(fa) = forced_align
278        && fa > struct_align
279    {
280        struct_align = fa;
281    }
282
283    if !packed && struct_align > 0 {
284        offset = offset.next_multiple_of(struct_align);
285    }
286
287    StructLayout {
288        name,
289        total_size: offset,
290        align: struct_align,
291        fields: out_fields,
292        source_file: None,
293        source_line: None,
294        arch,
295        is_packed: packed,
296        is_union: false,
297        is_repr_rust: false, // callers override this after construction
298    }
299}
300
301// ── visitor ───────────────────────────────────────────────────────────────────
302
303struct StructVisitor {
304    arch: &'static ArchConfig,
305    layouts: Vec<StructLayout>,
306}
307
308impl<'ast> Visit<'ast> for StructVisitor {
309    fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
310        syn::visit::visit_item_struct(self, node); // recurse into nested items
311
312        // Generic structs (e.g. `struct Foo<T>`) cannot be accurately laid out
313        // without knowing the concrete type arguments. Skip them rather than
314        // producing wrong field sizes for the type parameters.
315        if !node.generics.params.is_empty() {
316            return;
317        }
318
319        let name = node.ident.to_string();
320        let packed = is_packed(&node.attrs);
321        let forced_align = repr_align(&node.attrs);
322
323        // Collect (field_name, type, optional_guard)
324        let fields: Vec<(String, Type, Option<String>)> = match &node.fields {
325            Fields::Named(nf) => nf
326                .named
327                .iter()
328                .map(|f| {
329                    let fname = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
330                    let guard = extract_guard_from_attrs(&f.attrs);
331                    (fname, f.ty.clone(), guard)
332                })
333                .collect(),
334            Fields::Unnamed(uf) => uf
335                .unnamed
336                .iter()
337                .enumerate()
338                .map(|(i, f)| {
339                    let guard = extract_guard_from_attrs(&f.attrs);
340                    (format!("_{i}"), f.ty.clone(), guard)
341                })
342                .collect(),
343            Fields::Unit => vec![],
344        };
345
346        let name_ty: Vec<(String, Type)> = fields
347            .iter()
348            .map(|(n, t, _)| (n.clone(), t.clone()))
349            .collect();
350        let mut layout = simulate_rust_layout(name, &name_ty, packed, forced_align, self.arch);
351        layout.source_line = Some(node.ident.span().start().line as u32);
352        layout.is_repr_rust = is_repr_rust(&node.attrs);
353
354        // Apply explicit guard annotations; these take precedence over the
355        // heuristic type-name pass in concurrency.rs (which skips non-Unknown fields).
356        for (i, (_, _, guard)) in fields.iter().enumerate() {
357            if let Some(g) = guard {
358                layout.fields[i].access = AccessPattern::Concurrent {
359                    guard: Some(g.clone()),
360                    is_atomic: false,
361                };
362            }
363        }
364
365        self.layouts.push(layout);
366    }
367
368    fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
369        syn::visit::visit_item_enum(self, node);
370
371        // Skip generic enums (layout depends on unknown type arguments)
372        if !node.generics.params.is_empty() {
373            return;
374        }
375
376        let name = node.ident.to_string();
377        let n_variants = node.variants.len();
378        if n_variants == 0 {
379            return;
380        }
381
382        // Discriminant size: smallest integer that fits the variant count.
383        // Rust defaults to isize but uses the minimal repr in practice.
384        let disc_size: usize = if n_variants <= 256 {
385            1
386        } else if n_variants <= 65536 {
387            2
388        } else {
389            4
390        };
391
392        // Check if all variants are unit (C-like enum, no payload)
393        let all_unit = node
394            .variants
395            .iter()
396            .all(|v| matches!(v.fields, Fields::Unit));
397
398        if all_unit {
399            // Pure discriminant — no payload storage
400            let layout = StructLayout {
401                name,
402                total_size: disc_size,
403                align: disc_size,
404                fields: vec![Field {
405                    name: "__discriminant".to_string(),
406                    ty: TypeInfo::Primitive {
407                        name: format!("u{}", disc_size * 8),
408                        size: disc_size,
409                        align: disc_size,
410                    },
411                    offset: 0,
412                    size: disc_size,
413                    align: disc_size,
414                    source_file: None,
415                    source_line: None,
416                    access: AccessPattern::Unknown,
417                }],
418                source_file: None,
419                source_line: Some(node.ident.span().start().line as u32),
420                arch: self.arch,
421                is_packed: false,
422                is_union: false,
423                is_repr_rust: is_repr_rust(&node.attrs),
424            };
425            self.layouts.push(layout);
426            return;
427        }
428
429        // Data enum: find the maximum variant payload size and alignment.
430        let mut max_payload_size = 0usize;
431        let mut max_payload_align = 1usize;
432
433        for variant in &node.variants {
434            let var_fields: Vec<(String, Type)> = match &variant.fields {
435                Fields::Named(nf) => nf
436                    .named
437                    .iter()
438                    .map(|f| {
439                        let n = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
440                        (n, f.ty.clone())
441                    })
442                    .collect(),
443                Fields::Unnamed(uf) => uf
444                    .unnamed
445                    .iter()
446                    .enumerate()
447                    .map(|(i, f)| (format!("_{i}"), f.ty.clone()))
448                    .collect(),
449                Fields::Unit => vec![],
450            };
451
452            if !var_fields.is_empty() {
453                let var_layout =
454                    simulate_rust_layout(String::new(), &var_fields, false, None, self.arch);
455                if var_layout.total_size > max_payload_size {
456                    max_payload_size = var_layout.total_size;
457                }
458                max_payload_align = max_payload_align.max(var_layout.align);
459            }
460        }
461
462        // Conservative model: payload first at offset 0, discriminant immediately after.
463        // Rust's actual layout is compiler-controlled (niche optimisation etc.);
464        // this model gives a safe upper-bound for padding analysis.
465        let payload_align = max_payload_align.max(1);
466        let disc_offset = max_payload_size;
467        let total_before_pad = disc_offset + disc_size;
468        let total_align = payload_align.max(disc_size);
469        let total_size = total_before_pad.next_multiple_of(total_align);
470
471        let mut fields: Vec<Field> = Vec::new();
472        if max_payload_size > 0 {
473            fields.push(Field {
474                name: "__payload".to_string(),
475                ty: TypeInfo::Opaque {
476                    name: format!("largest_variant_payload ({}B)", max_payload_size),
477                    size: max_payload_size,
478                    align: payload_align,
479                },
480                offset: 0,
481                size: max_payload_size,
482                align: payload_align,
483                source_file: None,
484                source_line: None,
485                access: AccessPattern::Unknown,
486            });
487        }
488        fields.push(Field {
489            name: "__discriminant".to_string(),
490            ty: TypeInfo::Primitive {
491                name: format!("u{}", disc_size * 8),
492                size: disc_size,
493                align: disc_size,
494            },
495            offset: disc_offset,
496            size: disc_size,
497            align: disc_size,
498            source_file: None,
499            source_line: None,
500            access: AccessPattern::Unknown,
501        });
502
503        self.layouts.push(StructLayout {
504            name,
505            total_size,
506            align: total_align,
507            fields,
508            source_file: None,
509            source_line: Some(node.ident.span().start().line as u32),
510            arch: self.arch,
511            is_packed: false,
512            is_union: false,
513            is_repr_rust: is_repr_rust(&node.attrs),
514        });
515    }
516}
517
518// ── public API ────────────────────────────────────────────────────────────────
519
520pub fn parse_rust(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
521    let file: syn::File = syn::parse_str(source)?;
522    let mut visitor = StructVisitor {
523        arch,
524        layouts: Vec::new(),
525    };
526    visitor.visit_file(&file);
527    Ok(visitor.layouts)
528}
529
530// ── tests ─────────────────────────────────────────────────────────────────────
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use padlock_core::arch::X86_64_SYSV;
536
537    #[test]
538    fn parse_simple_struct() {
539        let src = "struct Foo { a: u8, b: u64, c: u32 }";
540        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
541        assert_eq!(layouts.len(), 1);
542        let l = &layouts[0];
543        assert_eq!(l.name, "Foo");
544        assert_eq!(l.fields.len(), 3);
545        assert_eq!(l.fields[0].size, 1); // u8
546        assert_eq!(l.fields[1].size, 8); // u64
547        assert_eq!(l.fields[2].size, 4); // u32
548    }
549
550    #[test]
551    fn layout_includes_padding() {
552        // u8 then u64: 7 bytes padding inserted
553        let src = "struct T { a: u8, b: u64 }";
554        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
555        let l = &layouts[0];
556        assert_eq!(l.fields[0].offset, 0);
557        assert_eq!(l.fields[1].offset, 8); // u64 aligned to 8
558        assert_eq!(l.total_size, 16);
559        let gaps = padlock_core::ir::find_padding(l);
560        assert_eq!(gaps[0].bytes, 7);
561    }
562
563    #[test]
564    fn multiple_structs_parsed() {
565        let src = "struct A { x: u32 } struct B { y: u64 }";
566        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
567        assert_eq!(layouts.len(), 2);
568    }
569
570    #[test]
571    fn packed_struct_no_padding() {
572        let src = "#[repr(packed)] struct P { a: u8, b: u64 }";
573        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
574        let l = &layouts[0];
575        assert!(l.is_packed);
576        assert_eq!(l.fields[1].offset, 1); // no padding, b immediately after a
577        let gaps = padlock_core::ir::find_padding(l);
578        assert!(gaps.is_empty());
579    }
580
581    #[test]
582    fn pointer_field_uses_arch_size() {
583        let src = "struct S { p: *const u8 }";
584        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
585        assert_eq!(layouts[0].fields[0].size, 8); // 64-bit pointer
586    }
587
588    // ── attribute guard extraction ─────────────────────────────────────────────
589
590    #[test]
591    fn lock_protected_by_attr_sets_guard() {
592        let src = r#"
593struct Cache {
594    #[lock_protected_by = "mu"]
595    readers: u64,
596    mu: u64,
597}
598"#;
599        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
600        let readers = &layouts[0].fields[0];
601        assert_eq!(readers.name, "readers");
602        if let AccessPattern::Concurrent { guard, .. } = &readers.access {
603            assert_eq!(guard.as_deref(), Some("mu"));
604        } else {
605            panic!("expected Concurrent, got {:?}", readers.access);
606        }
607    }
608
609    #[test]
610    fn guarded_by_string_attr_sets_guard() {
611        let src = r#"
612struct S {
613    #[guarded_by("lock")]
614    value: u32,
615}
616"#;
617        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
618        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
619            assert_eq!(guard.as_deref(), Some("lock"));
620        } else {
621            panic!("expected Concurrent");
622        }
623    }
624
625    #[test]
626    fn guarded_by_ident_attr_sets_guard() {
627        let src = r#"
628struct S {
629    #[guarded_by(mu)]
630    count: u64,
631}
632"#;
633        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
634        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
635            assert_eq!(guard.as_deref(), Some("mu"));
636        } else {
637            panic!("expected Concurrent");
638        }
639    }
640
641    #[test]
642    fn protected_by_attr_sets_guard() {
643        let src = r#"
644struct S {
645    #[protected_by = "lock_a"]
646    x: u64,
647}
648"#;
649        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
650        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
651            assert_eq!(guard.as_deref(), Some("lock_a"));
652        } else {
653            panic!("expected Concurrent");
654        }
655    }
656
657    #[test]
658    fn different_guards_on_same_cache_line_is_false_sharing() {
659        // readers and writers are at offsets 0 and 8 — same cache line (line 0).
660        // They have different explicit guards → confirmed false sharing.
661        let src = r#"
662struct HotPath {
663    #[lock_protected_by = "mu_a"]
664    readers: u64,
665    #[lock_protected_by = "mu_b"]
666    writers: u64,
667}
668"#;
669        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
670        assert!(padlock_core::analysis::false_sharing::has_false_sharing(
671            &layouts[0]
672        ));
673    }
674
675    #[test]
676    fn same_guard_on_same_cache_line_is_not_false_sharing() {
677        let src = r#"
678struct Safe {
679    #[lock_protected_by = "mu"]
680    a: u64,
681    #[lock_protected_by = "mu"]
682    b: u64,
683}
684"#;
685        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
686        assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
687            &layouts[0]
688        ));
689    }
690
691    #[test]
692    fn unannotated_field_stays_unknown() {
693        let src = "struct S { x: u64 }";
694        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
695        assert!(matches!(
696            layouts[0].fields[0].access,
697            AccessPattern::Unknown
698        ));
699    }
700
701    // ── stdlib type sizes ─────────────────────────────────────────────────────
702
703    #[test]
704    fn vec_field_has_three_pointer_size() {
705        // Vec<T> is always ptr + len + cap regardless of T
706        let src = "struct S { items: Vec<u64> }";
707        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
708        assert_eq!(layouts[0].fields[0].size, 24); // 3 × 8 on x86-64
709    }
710
711    #[test]
712    fn string_field_has_three_pointer_size() {
713        let src = "struct S { name: String }";
714        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
715        assert_eq!(layouts[0].fields[0].size, 24);
716    }
717
718    #[test]
719    fn box_field_has_pointer_size() {
720        let src = "struct S { inner: Box<u64> }";
721        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
722        assert_eq!(layouts[0].fields[0].size, 8);
723    }
724
725    #[test]
726    fn arc_field_has_pointer_size() {
727        let src = "struct S { shared: Arc<Vec<u8>> }";
728        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
729        assert_eq!(layouts[0].fields[0].size, 8);
730    }
731
732    #[test]
733    fn phantom_data_is_zero_sized() {
734        let src = "struct S { a: u64, _marker: PhantomData<u8> }";
735        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
736        let marker = layouts[0]
737            .fields
738            .iter()
739            .find(|f| f.name == "_marker")
740            .unwrap();
741        assert_eq!(marker.size, 0);
742    }
743
744    #[test]
745    fn duration_field_is_16_bytes() {
746        let src = "struct S { timeout: Duration }";
747        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
748        assert_eq!(layouts[0].fields[0].size, 16);
749    }
750
751    #[test]
752    fn atomic_u64_has_correct_size() {
753        let src = "struct S { counter: AtomicU64 }";
754        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
755        assert_eq!(layouts[0].fields[0].size, 8);
756    }
757
758    #[test]
759    fn atomic_bool_has_correct_size() {
760        let src = "struct S { flag: AtomicBool }";
761        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
762        assert_eq!(layouts[0].fields[0].size, 1);
763    }
764
765    // ── generic struct skipping ───────────────────────────────────────────────
766
767    #[test]
768    fn generic_struct_is_skipped() {
769        // Cannot accurately lay out struct Foo<T> without knowing T.
770        let src = "struct Wrapper<T> { value: T, count: usize }";
771        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
772        assert!(
773            layouts.is_empty(),
774            "generic structs should be skipped; got {:?}",
775            layouts.iter().map(|l| &l.name).collect::<Vec<_>>()
776        );
777    }
778
779    #[test]
780    fn generic_struct_with_multiple_params_is_skipped() {
781        let src = "struct Pair<A, B> { first: A, second: B }";
782        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
783        assert!(layouts.is_empty());
784    }
785
786    #[test]
787    fn non_generic_struct_still_parsed_when_generic_sibling_exists() {
788        let src = r#"
789struct Generic<T> { value: T }
790struct Concrete { a: u32, b: u64 }
791"#;
792        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
793        assert_eq!(layouts.len(), 1);
794        assert_eq!(layouts[0].name, "Concrete");
795    }
796
797    // ── enum data variant support ─────────────────────────────────────────────
798
799    #[test]
800    fn unit_enum_is_just_discriminant() {
801        let src = "enum Color { Red, Green, Blue }";
802        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
803        assert_eq!(layouts.len(), 1);
804        let l = &layouts[0];
805        assert_eq!(l.name, "Color");
806        assert_eq!(l.total_size, 1); // 3 variants → u8 discriminant
807        assert_eq!(l.fields.len(), 1);
808        assert_eq!(l.fields[0].name, "__discriminant");
809    }
810
811    #[test]
812    fn unit_enum_with_many_variants_uses_u16_discriminant() {
813        // Build an enum with 300 variants (> 256)
814        let variants: String = (0..300)
815            .map(|i| format!("V{i}"))
816            .collect::<Vec<_>>()
817            .join(", ");
818        let src = format!("enum Big {{ {variants} }}");
819        let layouts = parse_rust(&src, &X86_64_SYSV).unwrap();
820        let l = &layouts[0];
821        assert_eq!(l.total_size, 2); // needs u16
822        assert_eq!(l.fields[0].size, 2);
823    }
824
825    #[test]
826    fn data_enum_total_size_covers_largest_variant() {
827        // Quit: no payload; Move: {x: i32, y: i32} = 8B; Write: String = 24B
828        // Max payload = 24B (String), disc = 1B → total = 32B (aligned to 8)
829        let src = r#"
830enum Message {
831    Quit,
832    Move { x: i32, y: i32 },
833    Write(String),
834}
835"#;
836        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
837        let l = &layouts[0];
838        assert_eq!(l.name, "Message");
839        // __payload (24B, align 8) + __discriminant (1B) → padded to 32B
840        assert_eq!(l.total_size, 32);
841        assert_eq!(l.fields.len(), 2);
842        let payload = l.fields.iter().find(|f| f.name == "__payload").unwrap();
843        assert_eq!(payload.size, 24); // String = 3×pointer
844    }
845
846    #[test]
847    fn generic_enum_is_skipped() {
848        let src = "enum Wrapper<T> { Some(T), None }";
849        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
850        assert!(
851            layouts.is_empty(),
852            "generic enums should be skipped; got {:?}",
853            layouts.iter().map(|l| &l.name).collect::<Vec<_>>()
854        );
855    }
856
857    #[test]
858    fn empty_enum_is_skipped() {
859        let src = "enum Never {}";
860        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
861        assert!(layouts.is_empty());
862    }
863
864    #[test]
865    fn enum_with_only_unit_variants_has_no_payload_field() {
866        let src = "enum Dir { North, South, East, West }";
867        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
868        assert!(!layouts[0].fields.iter().any(|f| f.name == "__payload"));
869    }
870
871    #[test]
872    fn data_enum_and_sibling_struct_both_parsed() {
873        let src = r#"
874enum Status { Ok, Err(u32) }
875struct Conn { port: u16, status: u32 }
876"#;
877        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
878        assert_eq!(layouts.len(), 2);
879        assert!(layouts.iter().any(|l| l.name == "Status"));
880        assert!(layouts.iter().any(|l| l.name == "Conn"));
881    }
882
883    // ── bad weather: enums ────────────────────────────────────────────────────
884
885    #[test]
886    fn enum_with_only_zero_sized_variants_has_payload_size_zero() {
887        // All unit variants → treated as unit enum, total = disc_size
888        let src = "enum E { A, B }";
889        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
890        let l = &layouts[0];
891        assert_eq!(l.total_size, 1);
892    }
893
894    #[test]
895    fn enum_mixed_unit_and_data_includes_max_payload() {
896        // Mix: unit variant + data variant; payload comes from data variant
897        let src = "enum E { Nothing, Data(u64) }";
898        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
899        let l = &layouts[0];
900        let payload = l.fields.iter().find(|f| f.name == "__payload").unwrap();
901        assert_eq!(payload.size, 8); // u64
902    }
903
904    // ── repr(align(N)) ────────────────────────────────────────────────────────
905
906    #[test]
907    fn repr_align_raises_struct_alignment() {
908        let src = "#[repr(align(64))]\nstruct CacheLine { a: u8, b: u32 }";
909        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
910        let l = &layouts[0];
911        assert_eq!(
912            l.align, 64,
913            "repr(align(64)) must set struct alignment to 64"
914        );
915        assert_eq!(l.total_size, 64, "size must be padded to 64 bytes");
916    }
917
918    #[test]
919    fn repr_align_does_not_shrink_natural_alignment() {
920        // repr(align(1)) on a struct whose natural align is 8 — must keep 8
921        let src = "#[repr(align(1))]\nstruct S { a: u64 }";
922        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
923        let l = &layouts[0];
924        assert_eq!(
925            l.align, 8,
926            "natural align must not be reduced below repr(align)"
927        );
928    }
929
930    #[test]
931    fn repr_align_adds_trailing_padding() {
932        // u8 + u32 = 5 bytes natural, padded to 8 with align(8)
933        let src = "#[repr(align(8))]\nstruct S { a: u8, b: u32 }";
934        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
935        let l = &layouts[0];
936        assert_eq!(l.total_size, 8);
937    }
938
939    #[test]
940    fn no_repr_align_has_natural_size() {
941        // Baseline: without repr(align), just natural padding
942        let src = "struct S { a: u8, b: u32 }";
943        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
944        let l = &layouts[0];
945        // a:1 + 3 pad + b:4 = 8; align=4
946        assert_eq!(l.total_size, 8);
947        assert_eq!(l.align, 4);
948    }
949
950    // ── tuple structs ─────────────────────────────────────────────────────────
951
952    #[test]
953    fn tuple_struct_fields_named_by_index() {
954        let src = "struct Pair(u64, u8);";
955        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
956        let l = &layouts[0];
957        assert_eq!(l.fields[0].name, "_0");
958        assert_eq!(l.fields[1].name, "_1");
959    }
960
961    #[test]
962    fn tuple_struct_layout_follows_alignment() {
963        // u64 then u8: no padding before u64, 7 bytes trailing
964        let src = "struct S(u64, u8);";
965        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
966        let l = &layouts[0];
967        assert_eq!(l.fields[0].offset, 0);
968        assert_eq!(l.fields[0].size, 8);
969        assert_eq!(l.fields[1].offset, 8);
970        assert_eq!(l.fields[1].size, 1);
971        assert_eq!(l.total_size, 16);
972    }
973
974    #[test]
975    fn tuple_struct_with_padding_waste_detected() {
976        // u8 then u64: 7 bytes padding
977        let src = "struct S(u8, u64);";
978        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
979        let l = &layouts[0];
980        assert_eq!(l.fields[0].offset, 0); // u8 at 0
981        assert_eq!(l.fields[1].offset, 8); // u64 aligned to 8
982        assert_eq!(l.total_size, 16);
983        let gaps = padlock_core::ir::find_padding(l);
984        assert_eq!(gaps[0].bytes, 7);
985    }
986
987    // ── type-table tests ──────────────────────────────────────────────────────
988
989    #[test]
990    fn nonzero_types_same_size_as_base() {
991        assert_eq!(primitive_size_align("NonZeroU8", &X86_64_SYSV), (1, 1));
992        assert_eq!(primitive_size_align("NonZeroI8", &X86_64_SYSV), (1, 1));
993        assert_eq!(primitive_size_align("NonZeroU16", &X86_64_SYSV), (2, 2));
994        assert_eq!(primitive_size_align("NonZeroU32", &X86_64_SYSV), (4, 4));
995        assert_eq!(primitive_size_align("NonZeroU64", &X86_64_SYSV), (8, 8));
996        assert_eq!(primitive_size_align("NonZeroU128", &X86_64_SYSV), (16, 16));
997        assert_eq!(
998            primitive_size_align("NonZeroUsize", &X86_64_SYSV),
999            (X86_64_SYSV.pointer_size, X86_64_SYSV.pointer_size)
1000        );
1001    }
1002
1003    #[test]
1004    fn float16_and_float128_correct_size() {
1005        assert_eq!(primitive_size_align("f16", &X86_64_SYSV), (2, 2));
1006        assert_eq!(primitive_size_align("f128", &X86_64_SYSV), (16, 16));
1007    }
1008
1009    #[test]
1010    fn rust_struct_with_nonzero_fields() {
1011        let src = "struct Counts { hits: NonZeroU64, misses: NonZeroU32, flags: u8 }";
1012        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1013        let l = &layouts[0];
1014        assert_eq!(l.fields[0].size, 8); // NonZeroU64
1015        assert_eq!(l.fields[1].size, 4); // NonZeroU32
1016        assert_eq!(l.fields[2].size, 1); // u8
1017        // Total: 8+4+1 = 13, padded to align(8) = 16
1018        assert_eq!(l.total_size, 16);
1019    }
1020
1021    // ── repr(Rust) detection ──────────────────────────────────────────────────
1022
1023    #[test]
1024    fn plain_struct_is_repr_rust() {
1025        let src = "struct Foo { a: u64, b: u32 }";
1026        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1027        assert!(layouts[0].is_repr_rust, "plain struct should be repr(Rust)");
1028    }
1029
1030    #[test]
1031    fn repr_c_struct_is_not_repr_rust() {
1032        let src = "#[repr(C)] struct Foo { a: u64, b: u32 }";
1033        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1034        assert!(
1035            !layouts[0].is_repr_rust,
1036            "repr(C) struct must not be repr(Rust)"
1037        );
1038    }
1039
1040    #[test]
1041    fn repr_packed_struct_is_not_repr_rust() {
1042        let src = "#[repr(packed)] struct Foo { a: u64, b: u32 }";
1043        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1044        assert!(
1045            !layouts[0].is_repr_rust,
1046            "repr(packed) struct must not be repr(Rust)"
1047        );
1048    }
1049
1050    #[test]
1051    fn repr_transparent_struct_is_not_repr_rust() {
1052        let src = "#[repr(transparent)] struct Wrapper(u64);";
1053        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1054        assert!(
1055            !layouts[0].is_repr_rust,
1056            "repr(transparent) struct must not be repr(Rust)"
1057        );
1058    }
1059
1060    #[test]
1061    fn plain_enum_is_repr_rust() {
1062        let src = "enum Color { Red, Green, Blue }";
1063        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1064        assert!(layouts[0].is_repr_rust, "plain enum should be repr(Rust)");
1065    }
1066
1067    #[test]
1068    fn repr_c_enum_is_not_repr_rust() {
1069        let src = "#[repr(C)] enum Dir { North, South }";
1070        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1071        assert!(
1072            !layouts[0].is_repr_rust,
1073            "repr(C) enum must not be repr(Rust)"
1074        );
1075    }
1076}