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        suppressed_findings: Vec::new(), // callers may override after construction
299    }
300}
301
302// ── visitor ───────────────────────────────────────────────────────────────────
303
304struct StructVisitor<'src> {
305    arch: &'static ArchConfig,
306    layouts: Vec<StructLayout>,
307    source: &'src str,
308}
309
310impl<'ast, 'src> Visit<'ast> for StructVisitor<'src> {
311    fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
312        syn::visit::visit_item_struct(self, node); // recurse into nested items
313
314        // Generic structs (e.g. `struct Foo<T>`) cannot be accurately laid out
315        // without knowing the concrete type arguments. Skip them rather than
316        // producing wrong field sizes for the type parameters.
317        if !node.generics.params.is_empty() {
318            return;
319        }
320
321        let name = node.ident.to_string();
322        let packed = is_packed(&node.attrs);
323        let forced_align = repr_align(&node.attrs);
324
325        // Collect (field_name, type, optional_guard, source_line)
326        let fields: Vec<(String, Type, Option<String>, u32)> = match &node.fields {
327            Fields::Named(nf) => nf
328                .named
329                .iter()
330                .map(|f| {
331                    let fname = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
332                    let guard = extract_guard_from_attrs(&f.attrs);
333                    let line = f
334                        .ident
335                        .as_ref()
336                        .map(|i| i.span().start().line as u32)
337                        .unwrap_or(0);
338                    (fname, f.ty.clone(), guard, line)
339                })
340                .collect(),
341            Fields::Unnamed(uf) => uf
342                .unnamed
343                .iter()
344                .enumerate()
345                .map(|(i, f)| {
346                    let guard = extract_guard_from_attrs(&f.attrs);
347                    // Unnamed fields don't have an ident span; use 0 as a sentinel.
348                    (format!("_{i}"), f.ty.clone(), guard, 0u32)
349                })
350                .collect(),
351            Fields::Unit => vec![],
352        };
353
354        let name_ty: Vec<(String, Type)> = fields
355            .iter()
356            .map(|(n, t, _, _)| (n.clone(), t.clone()))
357            .collect();
358        let mut layout = simulate_rust_layout(name, &name_ty, packed, forced_align, self.arch);
359        let struct_line = node.ident.span().start().line as u32;
360        layout.source_line = Some(struct_line);
361        layout.is_repr_rust = is_repr_rust(&node.attrs);
362        layout.suppressed_findings =
363            super::suppress::suppressed_from_source_line(self.source, struct_line);
364
365        // Apply explicit guard annotations and field source lines.
366        for (i, (_, _, guard, field_line)) in fields.iter().enumerate() {
367            if *field_line > 0 {
368                layout.fields[i].source_line = Some(*field_line);
369            }
370            if let Some(g) = guard {
371                layout.fields[i].access = AccessPattern::Concurrent {
372                    guard: Some(g.clone()),
373                    is_atomic: false,
374                };
375            }
376        }
377
378        self.layouts.push(layout);
379    }
380
381    fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
382        syn::visit::visit_item_enum(self, node);
383
384        // Skip generic enums (layout depends on unknown type arguments)
385        if !node.generics.params.is_empty() {
386            return;
387        }
388
389        let name = node.ident.to_string();
390        let n_variants = node.variants.len();
391        if n_variants == 0 {
392            return;
393        }
394
395        // Discriminant size: smallest integer that fits the variant count.
396        // Rust defaults to isize but uses the minimal repr in practice.
397        let disc_size: usize = if n_variants <= 256 {
398            1
399        } else if n_variants <= 65536 {
400            2
401        } else {
402            4
403        };
404
405        // Check if all variants are unit (C-like enum, no payload)
406        let all_unit = node
407            .variants
408            .iter()
409            .all(|v| matches!(v.fields, Fields::Unit));
410
411        if all_unit {
412            // Pure discriminant — no payload storage
413            let enum_line = node.ident.span().start().line as u32;
414            let layout = StructLayout {
415                name,
416                total_size: disc_size,
417                align: disc_size,
418                fields: vec![Field {
419                    name: "__discriminant".to_string(),
420                    ty: TypeInfo::Primitive {
421                        name: format!("u{}", disc_size * 8),
422                        size: disc_size,
423                        align: disc_size,
424                    },
425                    offset: 0,
426                    size: disc_size,
427                    align: disc_size,
428                    source_file: None,
429                    source_line: None,
430                    access: AccessPattern::Unknown,
431                }],
432                source_file: None,
433                source_line: Some(enum_line),
434                arch: self.arch,
435                is_packed: false,
436                is_union: false,
437                is_repr_rust: is_repr_rust(&node.attrs),
438                suppressed_findings: super::suppress::suppressed_from_source_line(
439                    self.source,
440                    enum_line,
441                ),
442            };
443            self.layouts.push(layout);
444            return;
445        }
446
447        // Data enum: find the maximum variant payload size and alignment.
448        let mut max_payload_size = 0usize;
449        let mut max_payload_align = 1usize;
450
451        for variant in &node.variants {
452            let var_fields: Vec<(String, Type)> = match &variant.fields {
453                Fields::Named(nf) => nf
454                    .named
455                    .iter()
456                    .map(|f| {
457                        let n = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
458                        (n, f.ty.clone())
459                    })
460                    .collect(),
461                Fields::Unnamed(uf) => uf
462                    .unnamed
463                    .iter()
464                    .enumerate()
465                    .map(|(i, f)| (format!("_{i}"), f.ty.clone()))
466                    .collect(),
467                Fields::Unit => vec![],
468            };
469
470            if !var_fields.is_empty() {
471                let var_layout =
472                    simulate_rust_layout(String::new(), &var_fields, false, None, self.arch);
473                if var_layout.total_size > max_payload_size {
474                    max_payload_size = var_layout.total_size;
475                }
476                max_payload_align = max_payload_align.max(var_layout.align);
477            }
478        }
479
480        // Conservative model: payload first at offset 0, discriminant immediately after.
481        // Rust's actual layout is compiler-controlled (niche optimisation etc.);
482        // this model gives a safe upper-bound for padding analysis.
483        let payload_align = max_payload_align.max(1);
484        let disc_offset = max_payload_size;
485        let total_before_pad = disc_offset + disc_size;
486        let total_align = payload_align.max(disc_size);
487        let total_size = total_before_pad.next_multiple_of(total_align);
488
489        let mut fields: Vec<Field> = Vec::new();
490        if max_payload_size > 0 {
491            fields.push(Field {
492                name: "__payload".to_string(),
493                ty: TypeInfo::Opaque {
494                    name: format!("largest_variant_payload ({}B)", max_payload_size),
495                    size: max_payload_size,
496                    align: payload_align,
497                },
498                offset: 0,
499                size: max_payload_size,
500                align: payload_align,
501                source_file: None,
502                source_line: None,
503                access: AccessPattern::Unknown,
504            });
505        }
506        fields.push(Field {
507            name: "__discriminant".to_string(),
508            ty: TypeInfo::Primitive {
509                name: format!("u{}", disc_size * 8),
510                size: disc_size,
511                align: disc_size,
512            },
513            offset: disc_offset,
514            size: disc_size,
515            align: disc_size,
516            source_file: None,
517            source_line: None,
518            access: AccessPattern::Unknown,
519        });
520
521        let enum_line = node.ident.span().start().line as u32;
522        self.layouts.push(StructLayout {
523            name,
524            total_size,
525            align: total_align,
526            fields,
527            source_file: None,
528            source_line: Some(enum_line),
529            arch: self.arch,
530            is_packed: false,
531            is_union: false,
532            is_repr_rust: is_repr_rust(&node.attrs),
533            suppressed_findings: super::suppress::suppressed_from_source_line(
534                self.source,
535                enum_line,
536            ),
537        });
538    }
539}
540
541// ── public API ────────────────────────────────────────────────────────────────
542
543pub fn parse_rust(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
544    let file: syn::File = syn::parse_str(source)?;
545    let mut visitor = StructVisitor {
546        arch,
547        layouts: Vec::new(),
548        source,
549    };
550    visitor.visit_file(&file);
551    Ok(visitor.layouts)
552}
553
554// ── tests ─────────────────────────────────────────────────────────────────────
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use padlock_core::arch::X86_64_SYSV;
560
561    #[test]
562    fn parse_simple_struct() {
563        let src = "struct Foo { a: u8, b: u64, c: u32 }";
564        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
565        assert_eq!(layouts.len(), 1);
566        let l = &layouts[0];
567        assert_eq!(l.name, "Foo");
568        assert_eq!(l.fields.len(), 3);
569        assert_eq!(l.fields[0].size, 1); // u8
570        assert_eq!(l.fields[1].size, 8); // u64
571        assert_eq!(l.fields[2].size, 4); // u32
572    }
573
574    #[test]
575    fn layout_includes_padding() {
576        // u8 then u64: 7 bytes padding inserted
577        let src = "struct T { a: u8, b: u64 }";
578        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
579        let l = &layouts[0];
580        assert_eq!(l.fields[0].offset, 0);
581        assert_eq!(l.fields[1].offset, 8); // u64 aligned to 8
582        assert_eq!(l.total_size, 16);
583        let gaps = padlock_core::ir::find_padding(l);
584        assert_eq!(gaps[0].bytes, 7);
585    }
586
587    #[test]
588    fn multiple_structs_parsed() {
589        let src = "struct A { x: u32 } struct B { y: u64 }";
590        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
591        assert_eq!(layouts.len(), 2);
592    }
593
594    #[test]
595    fn packed_struct_no_padding() {
596        let src = "#[repr(packed)] struct P { a: u8, b: u64 }";
597        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
598        let l = &layouts[0];
599        assert!(l.is_packed);
600        assert_eq!(l.fields[1].offset, 1); // no padding, b immediately after a
601        let gaps = padlock_core::ir::find_padding(l);
602        assert!(gaps.is_empty());
603    }
604
605    #[test]
606    fn pointer_field_uses_arch_size() {
607        let src = "struct S { p: *const u8 }";
608        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
609        assert_eq!(layouts[0].fields[0].size, 8); // 64-bit pointer
610    }
611
612    // ── attribute guard extraction ─────────────────────────────────────────────
613
614    #[test]
615    fn lock_protected_by_attr_sets_guard() {
616        let src = r#"
617struct Cache {
618    #[lock_protected_by = "mu"]
619    readers: u64,
620    mu: u64,
621}
622"#;
623        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
624        let readers = &layouts[0].fields[0];
625        assert_eq!(readers.name, "readers");
626        if let AccessPattern::Concurrent { guard, .. } = &readers.access {
627            assert_eq!(guard.as_deref(), Some("mu"));
628        } else {
629            panic!("expected Concurrent, got {:?}", readers.access);
630        }
631    }
632
633    #[test]
634    fn guarded_by_string_attr_sets_guard() {
635        let src = r#"
636struct S {
637    #[guarded_by("lock")]
638    value: u32,
639}
640"#;
641        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
642        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
643            assert_eq!(guard.as_deref(), Some("lock"));
644        } else {
645            panic!("expected Concurrent");
646        }
647    }
648
649    #[test]
650    fn guarded_by_ident_attr_sets_guard() {
651        let src = r#"
652struct S {
653    #[guarded_by(mu)]
654    count: u64,
655}
656"#;
657        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
658        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
659            assert_eq!(guard.as_deref(), Some("mu"));
660        } else {
661            panic!("expected Concurrent");
662        }
663    }
664
665    #[test]
666    fn protected_by_attr_sets_guard() {
667        let src = r#"
668struct S {
669    #[protected_by = "lock_a"]
670    x: u64,
671}
672"#;
673        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
674        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
675            assert_eq!(guard.as_deref(), Some("lock_a"));
676        } else {
677            panic!("expected Concurrent");
678        }
679    }
680
681    #[test]
682    fn different_guards_on_same_cache_line_is_false_sharing() {
683        // readers and writers are at offsets 0 and 8 — same cache line (line 0).
684        // They have different explicit guards → confirmed false sharing.
685        let src = r#"
686struct HotPath {
687    #[lock_protected_by = "mu_a"]
688    readers: u64,
689    #[lock_protected_by = "mu_b"]
690    writers: u64,
691}
692"#;
693        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
694        assert!(padlock_core::analysis::false_sharing::has_false_sharing(
695            &layouts[0]
696        ));
697    }
698
699    #[test]
700    fn same_guard_on_same_cache_line_is_not_false_sharing() {
701        let src = r#"
702struct Safe {
703    #[lock_protected_by = "mu"]
704    a: u64,
705    #[lock_protected_by = "mu"]
706    b: u64,
707}
708"#;
709        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
710        assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
711            &layouts[0]
712        ));
713    }
714
715    #[test]
716    fn unannotated_field_stays_unknown() {
717        let src = "struct S { x: u64 }";
718        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
719        assert!(matches!(
720            layouts[0].fields[0].access,
721            AccessPattern::Unknown
722        ));
723    }
724
725    // ── stdlib type sizes ─────────────────────────────────────────────────────
726
727    #[test]
728    fn vec_field_has_three_pointer_size() {
729        // Vec<T> is always ptr + len + cap regardless of T
730        let src = "struct S { items: Vec<u64> }";
731        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
732        assert_eq!(layouts[0].fields[0].size, 24); // 3 × 8 on x86-64
733    }
734
735    #[test]
736    fn string_field_has_three_pointer_size() {
737        let src = "struct S { name: String }";
738        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
739        assert_eq!(layouts[0].fields[0].size, 24);
740    }
741
742    #[test]
743    fn box_field_has_pointer_size() {
744        let src = "struct S { inner: Box<u64> }";
745        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
746        assert_eq!(layouts[0].fields[0].size, 8);
747    }
748
749    #[test]
750    fn arc_field_has_pointer_size() {
751        let src = "struct S { shared: Arc<Vec<u8>> }";
752        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
753        assert_eq!(layouts[0].fields[0].size, 8);
754    }
755
756    #[test]
757    fn phantom_data_is_zero_sized() {
758        let src = "struct S { a: u64, _marker: PhantomData<u8> }";
759        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
760        let marker = layouts[0]
761            .fields
762            .iter()
763            .find(|f| f.name == "_marker")
764            .unwrap();
765        assert_eq!(marker.size, 0);
766    }
767
768    #[test]
769    fn duration_field_is_16_bytes() {
770        let src = "struct S { timeout: Duration }";
771        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
772        assert_eq!(layouts[0].fields[0].size, 16);
773    }
774
775    #[test]
776    fn atomic_u64_has_correct_size() {
777        let src = "struct S { counter: AtomicU64 }";
778        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
779        assert_eq!(layouts[0].fields[0].size, 8);
780    }
781
782    #[test]
783    fn atomic_bool_has_correct_size() {
784        let src = "struct S { flag: AtomicBool }";
785        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
786        assert_eq!(layouts[0].fields[0].size, 1);
787    }
788
789    // ── generic struct skipping ───────────────────────────────────────────────
790
791    #[test]
792    fn generic_struct_is_skipped() {
793        // Cannot accurately lay out struct Foo<T> without knowing T.
794        let src = "struct Wrapper<T> { value: T, count: usize }";
795        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
796        assert!(
797            layouts.is_empty(),
798            "generic structs should be skipped; got {:?}",
799            layouts.iter().map(|l| &l.name).collect::<Vec<_>>()
800        );
801    }
802
803    #[test]
804    fn generic_struct_with_multiple_params_is_skipped() {
805        let src = "struct Pair<A, B> { first: A, second: B }";
806        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
807        assert!(layouts.is_empty());
808    }
809
810    #[test]
811    fn non_generic_struct_still_parsed_when_generic_sibling_exists() {
812        let src = r#"
813struct Generic<T> { value: T }
814struct Concrete { a: u32, b: u64 }
815"#;
816        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
817        assert_eq!(layouts.len(), 1);
818        assert_eq!(layouts[0].name, "Concrete");
819    }
820
821    // ── enum data variant support ─────────────────────────────────────────────
822
823    #[test]
824    fn unit_enum_is_just_discriminant() {
825        let src = "enum Color { Red, Green, Blue }";
826        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
827        assert_eq!(layouts.len(), 1);
828        let l = &layouts[0];
829        assert_eq!(l.name, "Color");
830        assert_eq!(l.total_size, 1); // 3 variants → u8 discriminant
831        assert_eq!(l.fields.len(), 1);
832        assert_eq!(l.fields[0].name, "__discriminant");
833    }
834
835    #[test]
836    fn unit_enum_with_many_variants_uses_u16_discriminant() {
837        // Build an enum with 300 variants (> 256)
838        let variants: String = (0..300)
839            .map(|i| format!("V{i}"))
840            .collect::<Vec<_>>()
841            .join(", ");
842        let src = format!("enum Big {{ {variants} }}");
843        let layouts = parse_rust(&src, &X86_64_SYSV).unwrap();
844        let l = &layouts[0];
845        assert_eq!(l.total_size, 2); // needs u16
846        assert_eq!(l.fields[0].size, 2);
847    }
848
849    #[test]
850    fn data_enum_total_size_covers_largest_variant() {
851        // Quit: no payload; Move: {x: i32, y: i32} = 8B; Write: String = 24B
852        // Max payload = 24B (String), disc = 1B → total = 32B (aligned to 8)
853        let src = r#"
854enum Message {
855    Quit,
856    Move { x: i32, y: i32 },
857    Write(String),
858}
859"#;
860        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
861        let l = &layouts[0];
862        assert_eq!(l.name, "Message");
863        // __payload (24B, align 8) + __discriminant (1B) → padded to 32B
864        assert_eq!(l.total_size, 32);
865        assert_eq!(l.fields.len(), 2);
866        let payload = l.fields.iter().find(|f| f.name == "__payload").unwrap();
867        assert_eq!(payload.size, 24); // String = 3×pointer
868    }
869
870    #[test]
871    fn generic_enum_is_skipped() {
872        let src = "enum Wrapper<T> { Some(T), None }";
873        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
874        assert!(
875            layouts.is_empty(),
876            "generic enums should be skipped; got {:?}",
877            layouts.iter().map(|l| &l.name).collect::<Vec<_>>()
878        );
879    }
880
881    #[test]
882    fn empty_enum_is_skipped() {
883        let src = "enum Never {}";
884        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
885        assert!(layouts.is_empty());
886    }
887
888    #[test]
889    fn enum_with_only_unit_variants_has_no_payload_field() {
890        let src = "enum Dir { North, South, East, West }";
891        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
892        assert!(!layouts[0].fields.iter().any(|f| f.name == "__payload"));
893    }
894
895    #[test]
896    fn data_enum_and_sibling_struct_both_parsed() {
897        let src = r#"
898enum Status { Ok, Err(u32) }
899struct Conn { port: u16, status: u32 }
900"#;
901        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
902        assert_eq!(layouts.len(), 2);
903        assert!(layouts.iter().any(|l| l.name == "Status"));
904        assert!(layouts.iter().any(|l| l.name == "Conn"));
905    }
906
907    // ── bad weather: enums ────────────────────────────────────────────────────
908
909    #[test]
910    fn enum_with_only_zero_sized_variants_has_payload_size_zero() {
911        // All unit variants → treated as unit enum, total = disc_size
912        let src = "enum E { A, B }";
913        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
914        let l = &layouts[0];
915        assert_eq!(l.total_size, 1);
916    }
917
918    #[test]
919    fn enum_mixed_unit_and_data_includes_max_payload() {
920        // Mix: unit variant + data variant; payload comes from data variant
921        let src = "enum E { Nothing, Data(u64) }";
922        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
923        let l = &layouts[0];
924        let payload = l.fields.iter().find(|f| f.name == "__payload").unwrap();
925        assert_eq!(payload.size, 8); // u64
926    }
927
928    // ── repr(align(N)) ────────────────────────────────────────────────────────
929
930    #[test]
931    fn repr_align_raises_struct_alignment() {
932        let src = "#[repr(align(64))]\nstruct CacheLine { a: u8, b: u32 }";
933        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
934        let l = &layouts[0];
935        assert_eq!(
936            l.align, 64,
937            "repr(align(64)) must set struct alignment to 64"
938        );
939        assert_eq!(l.total_size, 64, "size must be padded to 64 bytes");
940    }
941
942    #[test]
943    fn repr_align_does_not_shrink_natural_alignment() {
944        // repr(align(1)) on a struct whose natural align is 8 — must keep 8
945        let src = "#[repr(align(1))]\nstruct S { a: u64 }";
946        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
947        let l = &layouts[0];
948        assert_eq!(
949            l.align, 8,
950            "natural align must not be reduced below repr(align)"
951        );
952    }
953
954    #[test]
955    fn repr_align_adds_trailing_padding() {
956        // u8 + u32 = 5 bytes natural, padded to 8 with align(8)
957        let src = "#[repr(align(8))]\nstruct S { a: u8, b: u32 }";
958        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
959        let l = &layouts[0];
960        assert_eq!(l.total_size, 8);
961    }
962
963    #[test]
964    fn no_repr_align_has_natural_size() {
965        // Baseline: without repr(align), just natural padding
966        let src = "struct S { a: u8, b: u32 }";
967        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
968        let l = &layouts[0];
969        // a:1 + 3 pad + b:4 = 8; align=4
970        assert_eq!(l.total_size, 8);
971        assert_eq!(l.align, 4);
972    }
973
974    // ── tuple structs ─────────────────────────────────────────────────────────
975
976    #[test]
977    fn tuple_struct_fields_named_by_index() {
978        let src = "struct Pair(u64, u8);";
979        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
980        let l = &layouts[0];
981        assert_eq!(l.fields[0].name, "_0");
982        assert_eq!(l.fields[1].name, "_1");
983    }
984
985    #[test]
986    fn tuple_struct_layout_follows_alignment() {
987        // u64 then u8: no padding before u64, 7 bytes trailing
988        let src = "struct S(u64, u8);";
989        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
990        let l = &layouts[0];
991        assert_eq!(l.fields[0].offset, 0);
992        assert_eq!(l.fields[0].size, 8);
993        assert_eq!(l.fields[1].offset, 8);
994        assert_eq!(l.fields[1].size, 1);
995        assert_eq!(l.total_size, 16);
996    }
997
998    #[test]
999    fn tuple_struct_with_padding_waste_detected() {
1000        // u8 then u64: 7 bytes padding
1001        let src = "struct S(u8, u64);";
1002        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1003        let l = &layouts[0];
1004        assert_eq!(l.fields[0].offset, 0); // u8 at 0
1005        assert_eq!(l.fields[1].offset, 8); // u64 aligned to 8
1006        assert_eq!(l.total_size, 16);
1007        let gaps = padlock_core::ir::find_padding(l);
1008        assert_eq!(gaps[0].bytes, 7);
1009    }
1010
1011    // ── type-table tests ──────────────────────────────────────────────────────
1012
1013    #[test]
1014    fn nonzero_types_same_size_as_base() {
1015        assert_eq!(primitive_size_align("NonZeroU8", &X86_64_SYSV), (1, 1));
1016        assert_eq!(primitive_size_align("NonZeroI8", &X86_64_SYSV), (1, 1));
1017        assert_eq!(primitive_size_align("NonZeroU16", &X86_64_SYSV), (2, 2));
1018        assert_eq!(primitive_size_align("NonZeroU32", &X86_64_SYSV), (4, 4));
1019        assert_eq!(primitive_size_align("NonZeroU64", &X86_64_SYSV), (8, 8));
1020        assert_eq!(primitive_size_align("NonZeroU128", &X86_64_SYSV), (16, 16));
1021        assert_eq!(
1022            primitive_size_align("NonZeroUsize", &X86_64_SYSV),
1023            (X86_64_SYSV.pointer_size, X86_64_SYSV.pointer_size)
1024        );
1025    }
1026
1027    #[test]
1028    fn float16_and_float128_correct_size() {
1029        assert_eq!(primitive_size_align("f16", &X86_64_SYSV), (2, 2));
1030        assert_eq!(primitive_size_align("f128", &X86_64_SYSV), (16, 16));
1031    }
1032
1033    #[test]
1034    fn rust_struct_with_nonzero_fields() {
1035        let src = "struct Counts { hits: NonZeroU64, misses: NonZeroU32, flags: u8 }";
1036        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1037        let l = &layouts[0];
1038        assert_eq!(l.fields[0].size, 8); // NonZeroU64
1039        assert_eq!(l.fields[1].size, 4); // NonZeroU32
1040        assert_eq!(l.fields[2].size, 1); // u8
1041        // Total: 8+4+1 = 13, padded to align(8) = 16
1042        assert_eq!(l.total_size, 16);
1043    }
1044
1045    // ── repr(Rust) detection ──────────────────────────────────────────────────
1046
1047    #[test]
1048    fn plain_struct_is_repr_rust() {
1049        let src = "struct Foo { a: u64, b: u32 }";
1050        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1051        assert!(layouts[0].is_repr_rust, "plain struct should be repr(Rust)");
1052    }
1053
1054    #[test]
1055    fn repr_c_struct_is_not_repr_rust() {
1056        let src = "#[repr(C)] struct Foo { a: u64, b: u32 }";
1057        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1058        assert!(
1059            !layouts[0].is_repr_rust,
1060            "repr(C) struct must not be repr(Rust)"
1061        );
1062    }
1063
1064    #[test]
1065    fn repr_packed_struct_is_not_repr_rust() {
1066        let src = "#[repr(packed)] struct Foo { a: u64, b: u32 }";
1067        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1068        assert!(
1069            !layouts[0].is_repr_rust,
1070            "repr(packed) struct must not be repr(Rust)"
1071        );
1072    }
1073
1074    #[test]
1075    fn repr_transparent_struct_is_not_repr_rust() {
1076        let src = "#[repr(transparent)] struct Wrapper(u64);";
1077        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1078        assert!(
1079            !layouts[0].is_repr_rust,
1080            "repr(transparent) struct must not be repr(Rust)"
1081        );
1082    }
1083
1084    #[test]
1085    fn plain_enum_is_repr_rust() {
1086        let src = "enum Color { Red, Green, Blue }";
1087        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1088        assert!(layouts[0].is_repr_rust, "plain enum should be repr(Rust)");
1089    }
1090
1091    #[test]
1092    fn repr_c_enum_is_not_repr_rust() {
1093        let src = "#[repr(C)] enum Dir { North, South }";
1094        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1095        assert!(
1096            !layouts[0].is_repr_rust,
1097            "repr(C) enum must not be repr(Rust)"
1098        );
1099    }
1100}