Skip to main content

padlock_source/frontends/
zig.rs

1// padlock-source/src/frontends/zig.rs
2//
3// Extracts struct layouts from Zig source using tree-sitter-zig.
4// Handles regular, extern, and packed struct variants.
5// Sizes use Zig's platform-native alignment rules (same as C on the target arch).
6
7use padlock_core::arch::ArchConfig;
8use padlock_core::ir::{Field, StructLayout, TypeInfo};
9use tree_sitter::{Node, Parser};
10
11// ── type resolution ───────────────────────────────────────────────────────────
12
13fn zig_type_size_align(ty: &str, arch: &'static ArchConfig) -> (usize, usize) {
14    let ty = ty.trim();
15    match ty {
16        "bool" => (1, 1),
17        "u8" | "i8" => (1, 1),
18        "u16" | "i16" | "f16" => (2, 2),
19        "u32" | "i32" | "f32" => (4, 4),
20        "u64" | "i64" | "f64" => (8, 8),
21        "u128" | "i128" | "f128" => (16, 16),
22        // f80 is the x87 80-bit float; stored as 10 bytes, aligned to 16 on x86-64
23        "f80" => (10, 16),
24        "usize" | "isize" => (arch.pointer_size, arch.pointer_size),
25        "void" | "anyopaque" => (0, 1),
26        // comptime-only or type-erased — treat as pointer-sized
27        "type" | "anytype" | "comptime_int" | "comptime_float" => {
28            (arch.pointer_size, arch.pointer_size)
29        }
30
31        // Zig C-interop types (std.c / @cImport equivalents)
32        "c_char" | "c_uchar" | "c_schar" => (1, 1),
33        "c_short" | "c_ushort" => (2, 2),
34        "c_int" | "c_uint" => (4, 4),
35        "c_long" | "c_ulong" => (arch.pointer_size, arch.pointer_size), // 8B on LP64, 4B on LLP64
36        "c_longlong" | "c_ulonglong" => (8, 8),
37        "c_float" => (4, 4),
38        "c_double" => (8, 8),
39        "c_longdouble" => (16, 16),
40
41        // Arbitrary-width integers: uN / iN where N is a decimal number.
42        // In an extern struct the field occupies ceil(N/8) bytes, aligned to
43        // the next power-of-two (capped at 8). In a packed struct all integers
44        // are bit-packed — we cannot model that without knowing the context, so
45        // we use the same ceil-and-align heuristic as a reasonable approximation.
46        ty if (ty.starts_with('u') || ty.starts_with('i'))
47            && ty[1..].bytes().all(|b| b.is_ascii_digit())
48            && !ty[1..].is_empty() =>
49        {
50            if let Ok(bits) = ty[1..].parse::<usize>() {
51                let bytes = bits.div_ceil(8).max(1);
52                // Align to next power-of-two, capped at 8
53                let align = bytes.next_power_of_two().min(8);
54                (bytes, align)
55            } else {
56                (arch.pointer_size, arch.pointer_size)
57            }
58        }
59
60        _ => (arch.pointer_size, arch.pointer_size),
61    }
62}
63
64/// Return the exact bit width of a Zig type inside a `packed struct`.
65///
66/// In a packed struct every field is bit-packed without inter-field padding.
67/// Arbitrary-width integers (`u3`, `i11`, …) occupy exactly N bits; all other
68/// types occupy their natural byte size × 8 bits.
69fn zig_type_bit_width(ty: &str, arch: &'static ArchConfig) -> usize {
70    let ty = ty.trim();
71    // Arbitrary-width integers: exact bit count
72    if (ty.starts_with('u') || ty.starts_with('i'))
73        && !ty[1..].is_empty()
74        && ty[1..].bytes().all(|b| b.is_ascii_digit())
75        && let Ok(bits) = ty[1..].parse::<usize>()
76    {
77        return bits;
78    }
79    match ty {
80        "bool" => 1,
81        "f16" => 16,
82        "f32" => 32,
83        "f64" => 64,
84        "f80" => 80,
85        "f128" => 128,
86        _ => {
87            // Fall back to byte size × 8
88            let (bytes, _) = zig_type_size_align(ty, arch);
89            bytes * 8
90        }
91    }
92}
93
94/// Determine size/align of a type node, dispatching by node kind.
95fn type_node_size_align(source: &str, node: Node<'_>, arch: &'static ArchConfig) -> (usize, usize) {
96    match node.kind() {
97        "builtin_type" | "identifier" => {
98            let text = source[node.byte_range()].trim();
99            zig_type_size_align(text, arch)
100        }
101        // *T — single pointer
102        "pointer_type" => (arch.pointer_size, arch.pointer_size),
103        // ?T — optional; if T is a pointer the optional is pointer-sized (null = 0),
104        // otherwise it is T + 1 byte tag, rounded up. Approximate as pointer-sized.
105        "nullable_type" => {
106            // Check if the inner type is a pointer — if so, null-pointer optimisation applies
107            if let Some(inner) = find_child_by_kinds(node, &["pointer_type"]) {
108                let _ = inner; // pointer optionals are pointer-sized
109                (arch.pointer_size, arch.pointer_size)
110            } else if let Some(inner) = find_first_type_child(source, node) {
111                let (sz, al) = type_node_size_align(source, inner, arch);
112                // Add 1 byte tag, round up to alignment
113                let tagged = (sz + 1).next_multiple_of(al.max(1));
114                (tagged, al.max(1))
115            } else {
116                (arch.pointer_size, arch.pointer_size)
117            }
118        }
119        // []T — slice = (ptr, len)
120        "slice_type" => (arch.pointer_size * 2, arch.pointer_size),
121        // [N]T — array; try to parse N and recursively get element size
122        "array_type" => {
123            if let Some((count, elem_sz, elem_al)) = parse_array_type(source, node, arch) {
124                (elem_sz * count, elem_al)
125            } else {
126                (arch.pointer_size, arch.pointer_size)
127            }
128        }
129        // error union E!T — approximate as two words
130        "error_union" => (arch.pointer_size * 2, arch.pointer_size),
131        _ => (arch.pointer_size, arch.pointer_size),
132    }
133}
134
135/// For `[N]T` nodes, return `Some((count, elem_size, elem_align))`.
136fn parse_array_type(
137    source: &str,
138    node: Node<'_>,
139    arch: &'static ArchConfig,
140) -> Option<(usize, usize, usize)> {
141    // array_type children: [ integer_literal ] type_expr
142    let mut count: Option<usize> = None;
143    let mut elem: Option<(usize, usize)> = None;
144
145    for i in 0..node.child_count() {
146        let child = node.child(i)?;
147        match child.kind() {
148            "integer" | "integer_literal" => {
149                let text = source[child.byte_range()].trim();
150                count = text.parse::<usize>().ok();
151            }
152            "builtin_type" | "identifier" | "pointer_type" | "slice_type" | "array_type"
153            | "nullable_type" => {
154                elem = Some(type_node_size_align(source, child, arch));
155            }
156            _ => {}
157        }
158    }
159
160    let count = count?;
161    let (esz, eal) = elem.unwrap_or((arch.pointer_size, arch.pointer_size));
162    Some((count, esz, eal))
163}
164
165fn find_child_by_kinds<'a>(node: Node<'a>, kinds: &[&str]) -> Option<Node<'a>> {
166    for i in 0..node.child_count() {
167        if let Some(c) = node.child(i)
168            && kinds.contains(&c.kind())
169        {
170            return Some(c);
171        }
172    }
173    None
174}
175
176fn find_first_type_child<'a>(source: &str, node: Node<'a>) -> Option<Node<'a>> {
177    let _ = source;
178    for i in 0..node.child_count() {
179        if let Some(c) = node.child(i) {
180            match c.kind() {
181                "builtin_type" | "identifier" | "pointer_type" | "slice_type" | "array_type"
182                | "nullable_type" | "error_union" => return Some(c),
183                _ => {}
184            }
185        }
186    }
187    None
188}
189
190// ── tree-sitter walker ────────────────────────────────────────────────────────
191
192fn extract_structs(source: &str, root: Node<'_>, arch: &'static ArchConfig) -> Vec<StructLayout> {
193    let mut layouts = Vec::new();
194    let mut stack = vec![root];
195
196    while let Some(node) = stack.pop() {
197        for i in (0..node.child_count()).rev() {
198            if let Some(c) = node.child(i) {
199                stack.push(c);
200            }
201        }
202
203        if node.kind() == "variable_declaration"
204            && let Some(layout) = parse_variable_declaration(source, node, arch)
205        {
206            layouts.push(layout);
207        }
208    }
209    layouts
210}
211
212fn parse_variable_declaration(
213    source: &str,
214    node: Node<'_>,
215    arch: &'static ArchConfig,
216) -> Option<StructLayout> {
217    let source_line = node.start_position().row as u32 + 1;
218    let decl_start_byte = node.start_byte();
219    let mut name: Option<String> = None;
220    let mut struct_node: Option<Node> = None;
221    let mut union_node: Option<Node> = None;
222
223    for i in 0..node.child_count() {
224        let child = node.child(i)?;
225        match child.kind() {
226            "identifier" if name.is_none() => {
227                // The first identifier after `const`/`var` is the name
228                name = Some(source[child.byte_range()].to_string());
229            }
230            "struct_declaration" => struct_node = Some(child),
231            "union_declaration" => union_node = Some(child),
232            _ => {}
233        }
234    }
235
236    let name = name?;
237    let mut layout = if let Some(sn) = struct_node {
238        parse_struct_declaration(source, sn, name, arch, source_line)?
239    } else if let Some(un) = union_node {
240        parse_union_declaration(source, un, name, arch, source_line)?
241    } else {
242        return None;
243    };
244    layout.suppressed_findings =
245        super::suppress::suppressed_from_preceding_source(source, decl_start_byte);
246    Some(layout)
247}
248
249/// Parse a Zig `union { ... }` or `union(enum) { ... }` declaration.
250///
251/// Layout rules:
252/// - All fields share the same storage (offset 0), total = max(field sizes).
253/// - Tagged unions add a synthetic `__tag` discriminant field; its size is
254///   the smallest integer that covers the variant count.
255/// - The struct is emitted with `is_union = true`.
256fn parse_union_declaration(
257    source: &str,
258    node: Node<'_>,
259    name: String,
260    arch: &'static ArchConfig,
261    source_line: u32,
262) -> Option<StructLayout> {
263    let mut is_tagged = false;
264    let mut raw_fields: Vec<(String, String, usize, usize, u32)> = Vec::new();
265
266    for i in 0..node.child_count() {
267        let child = node.child(i)?;
268        match child.kind() {
269            // `union(enum)` — `enum` keyword is a direct child
270            "enum" => is_tagged = true,
271            // `union(SomeEnum)` — identifier naming the explicit tag type
272            // We detect this by seeing an identifier inside the `(...)` group.
273            // Mark it as tagged regardless of the tag type.
274            "container_field" => {
275                if let Some(f) = parse_container_field(source, child, arch, false) {
276                    raw_fields.push(f);
277                }
278            }
279            _ => {}
280        }
281    }
282
283    if raw_fields.is_empty() {
284        return None;
285    }
286
287    // Union layout: all fields at offset 0; total = max field size rounded to alignment.
288    let max_size = raw_fields
289        .iter()
290        .map(|(_, _, sz, _, _)| *sz)
291        .max()
292        .unwrap_or(0);
293    let max_align = raw_fields
294        .iter()
295        .map(|(_, _, _, al, _)| *al)
296        .max()
297        .unwrap_or(1);
298    let total_size = if max_align > 0 {
299        max_size.next_multiple_of(max_align)
300    } else {
301        max_size
302    };
303
304    let mut fields: Vec<Field> = raw_fields
305        .into_iter()
306        .map(|(fname, type_text, size, align, field_line)| Field {
307            name: fname,
308            ty: TypeInfo::Primitive {
309                name: type_text,
310                size,
311                align,
312            },
313            offset: 0,
314            size,
315            align,
316            source_file: None,
317            source_line: Some(field_line),
318            access: padlock_core::ir::AccessPattern::Unknown,
319        })
320        .collect();
321
322    // Tagged union: add a synthetic `__tag` discriminant field.
323    // Its size is the smallest integer type that holds all variant indices.
324    if is_tagged {
325        let n = fields.len();
326        let tag_size: usize = if n <= 256 {
327            1
328        } else if n <= 65536 {
329            2
330        } else {
331            4
332        };
333        fields.push(Field {
334            name: "__tag".to_string(),
335            ty: TypeInfo::Primitive {
336                name: format!("u{}", tag_size * 8),
337                size: tag_size,
338                align: tag_size,
339            },
340            offset: total_size, // tag lives after the union payload
341            size: tag_size,
342            align: tag_size,
343            source_file: None,
344            source_line: None,
345            access: padlock_core::ir::AccessPattern::Unknown,
346        });
347    }
348
349    let struct_align = max_align; // tag alignment is usually smaller than payload
350
351    let final_size = if is_tagged {
352        let tag_size = fields.last().map(|f| f.size).unwrap_or(0);
353        (total_size + tag_size).next_multiple_of(struct_align.max(1))
354    } else {
355        total_size
356    };
357
358    Some(StructLayout {
359        name,
360        total_size: final_size,
361        align: struct_align,
362        fields,
363        source_file: None,
364        source_line: Some(source_line),
365        arch,
366        is_packed: false,
367        is_union: true,
368        is_repr_rust: false,
369        suppressed_findings: Vec::new(), // set by parse_variable_declaration
370        uncertain_fields: Vec::new(),
371    })
372}
373
374fn parse_struct_declaration(
375    source: &str,
376    node: Node<'_>,
377    name: String,
378    arch: &'static ArchConfig,
379    source_line: u32,
380) -> Option<StructLayout> {
381    let mut is_packed = false;
382    let mut is_extern = false;
383    // (field_name, type_text, size, align, source_line)
384    let mut raw_fields: Vec<(String, String, usize, usize, u32)> = Vec::new();
385
386    for i in 0..node.child_count() {
387        let child = node.child(i)?;
388        match child.kind() {
389            "packed" => is_packed = true,
390            "extern" => is_extern = true,
391            "container_field" => {
392                if let Some(f) = parse_container_field(source, child, arch, is_packed) {
393                    raw_fields.push(f);
394                }
395            }
396            _ => {}
397        }
398    }
399
400    if raw_fields.is_empty() {
401        return None;
402    }
403
404    // Regular Zig structs have implementation-defined layout (reordering allowed).
405    // Only extern and packed structs have stable C-compatible / bit-exact layout.
406    // For analysis purposes we simulate the declared order for all variants,
407    // since that is what the developer sees and intends to reason about.
408    let mut offset = 0usize;
409    let mut struct_align = 1usize;
410    let mut fields: Vec<Field> = Vec::new();
411
412    if is_packed {
413        // Packed structs: fields are bit-packed with no inter-field padding.
414        // Track the running bit offset; convert to byte offset for Field.offset.
415        // Total struct size = ceil(total_bits / 8).
416        let mut bit_offset = 0usize;
417        for (fname, type_text, _byte_size, _byte_align, field_line) in &raw_fields {
418            let bit_width = zig_type_bit_width(type_text, arch);
419            let byte_offset = bit_offset / 8;
420            let byte_size = bit_width
421                .div_ceil(8)
422                .max(if bit_width == 0 { 0 } else { 1 });
423            fields.push(Field {
424                name: fname.clone(),
425                ty: TypeInfo::Primitive {
426                    name: type_text.clone(),
427                    size: byte_size,
428                    align: 1,
429                },
430                offset: byte_offset,
431                size: byte_size,
432                align: 1,
433                source_file: None,
434                source_line: Some(*field_line),
435                access: padlock_core::ir::AccessPattern::Unknown,
436            });
437            bit_offset += bit_width;
438        }
439        offset = bit_offset.div_ceil(8);
440        struct_align = 1;
441    } else {
442        for (fname, type_text, size, align, field_line) in raw_fields {
443            if align > 0 {
444                offset = offset.next_multiple_of(align);
445            }
446            struct_align = struct_align.max(align);
447            fields.push(Field {
448                name: fname,
449                ty: TypeInfo::Primitive {
450                    name: type_text,
451                    size,
452                    align,
453                },
454                offset,
455                size,
456                align,
457                source_file: None,
458                source_line: Some(field_line),
459                access: padlock_core::ir::AccessPattern::Unknown,
460            });
461            offset += size;
462        }
463        if struct_align > 0 {
464            offset = offset.next_multiple_of(struct_align);
465        }
466    }
467
468    let _ = is_extern; // affects ABI guarantees, not layout simulation
469
470    Some(StructLayout {
471        name,
472        total_size: offset,
473        align: struct_align,
474        fields,
475        source_file: None,
476        source_line: Some(source_line),
477        arch,
478        is_packed,
479        is_union: false,
480        is_repr_rust: false,
481        suppressed_findings: Vec::new(), // set by parse_variable_declaration
482        uncertain_fields: Vec::new(),
483    })
484}
485
486/// Parse a `container_field` node and return `(name, type_text, size, align, source_line)`.
487fn parse_container_field(
488    source: &str,
489    node: Node<'_>,
490    arch: &'static ArchConfig,
491    is_packed: bool,
492) -> Option<(String, String, usize, usize, u32)> {
493    let mut field_name: Option<String> = None;
494    let mut type_text: Option<String> = None;
495    let mut size_align: Option<(usize, usize)> = None;
496
497    for i in 0..node.child_count() {
498        let child = node.child(i)?;
499        match child.kind() {
500            "identifier" if field_name.is_none() => {
501                field_name = Some(source[child.byte_range()].to_string());
502            }
503            "builtin_type" | "pointer_type" | "nullable_type" | "slice_type" | "array_type"
504            | "error_union" => {
505                let text = source[child.byte_range()].to_string();
506                size_align = Some(type_node_size_align(source, child, arch));
507                type_text = Some(text);
508            }
509            "identifier" => {
510                // Second identifier = type name (e.g. a named struct type)
511                let text = source[child.byte_range()].trim().to_string();
512                size_align = Some(zig_type_size_align(&text, arch));
513                type_text = Some(text);
514            }
515            _ => {}
516        }
517    }
518
519    // Discard fields with empty names — tree-sitter-zig emits a zero-length
520    // identifier node for `union {}` (empty union body), which is not a real field.
521    let name = field_name.filter(|n| !n.is_empty())?;
522    let ty = type_text.unwrap_or_else(|| "anyopaque".to_string());
523    let (mut size, align) = size_align.unwrap_or((arch.pointer_size, arch.pointer_size));
524    let field_line = node.start_position().row as u32 + 1;
525
526    if is_packed && size == 0 {
527        size = 0; // void fields in packed structs stay 0
528    }
529
530    Some((name, ty, size, align, field_line))
531}
532
533// ── public API ────────────────────────────────────────────────────────────────
534
535pub fn parse_zig(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
536    let mut parser = Parser::new();
537    parser.set_language(&tree_sitter_zig::LANGUAGE.into())?;
538    let tree = parser
539        .parse(source, None)
540        .ok_or_else(|| anyhow::anyhow!("tree-sitter-zig parse failed"))?;
541    Ok(extract_structs(source, tree.root_node(), arch))
542}
543
544// ── tests ─────────────────────────────────────────────────────────────────────
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549    use padlock_core::arch::X86_64_SYSV;
550
551    #[test]
552    fn parse_simple_zig_struct() {
553        let src = "const Point = struct { x: u32, y: u32 };";
554        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
555        assert_eq!(layouts.len(), 1);
556        assert_eq!(layouts[0].name, "Point");
557        assert_eq!(layouts[0].fields.len(), 2);
558        assert_eq!(layouts[0].total_size, 8);
559    }
560
561    #[test]
562    fn zig_layout_with_padding() {
563        let src = "const T = struct { a: bool, b: u64 };";
564        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
565        assert_eq!(layouts.len(), 1);
566        let l = &layouts[0];
567        assert_eq!(l.fields[0].offset, 0); // bool at 0
568        assert_eq!(l.fields[1].offset, 8); // u64 at 8 (7 bytes padding)
569        assert_eq!(l.total_size, 16);
570    }
571
572    #[test]
573    fn zig_packed_struct_no_padding() {
574        let src = "const Packed = packed struct { a: u8, b: u32 };";
575        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
576        assert_eq!(layouts.len(), 1);
577        let l = &layouts[0];
578        assert!(l.is_packed);
579        assert_eq!(l.fields[0].offset, 0);
580        assert_eq!(l.fields[1].offset, 1); // immediately after u8, no padding
581        assert_eq!(l.total_size, 5);
582    }
583
584    #[test]
585    fn zig_extern_struct_detected() {
586        let src = "const Extern = extern struct { x: i32, y: f64 };";
587        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
588        assert_eq!(layouts.len(), 1);
589        let l = &layouts[0];
590        // extern struct has C layout: x at 0 (4B), 4B pad, y at 8 (8B)
591        assert_eq!(l.fields[0].offset, 0);
592        assert_eq!(l.fields[1].offset, 8);
593        assert_eq!(l.total_size, 16);
594    }
595
596    #[test]
597    fn zig_pointer_field_is_pointer_sized() {
598        let src = "const S = struct { ptr: *u8 };";
599        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
600        assert_eq!(layouts[0].fields[0].size, 8);
601        assert_eq!(layouts[0].fields[0].align, 8);
602    }
603
604    #[test]
605    fn zig_optional_pointer_is_pointer_sized() {
606        let src = "const S = struct { opt: ?*u8 };";
607        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
608        assert_eq!(layouts[0].fields[0].size, 8);
609    }
610
611    #[test]
612    fn zig_slice_is_two_words() {
613        let src = "const S = struct { buf: []u8 };";
614        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
615        assert_eq!(layouts[0].fields[0].size, 16); // ptr + len
616    }
617
618    #[test]
619    fn zig_usize_follows_arch() {
620        let src = "const S = struct { n: usize };";
621        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
622        assert_eq!(layouts[0].fields[0].size, 8);
623    }
624
625    #[test]
626    fn zig_multiple_structs_parsed() {
627        let src = "const A = struct { x: u8 };\nconst B = struct { y: u64 };";
628        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
629        assert_eq!(layouts.len(), 2);
630        assert!(layouts.iter().any(|l| l.name == "A"));
631        assert!(layouts.iter().any(|l| l.name == "B"));
632    }
633
634    #[test]
635    fn zig_array_field_size() {
636        let src = "const S = struct { buf: [4]u32 };";
637        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
638        assert_eq!(layouts[0].fields[0].size, 16); // 4 * 4
639    }
640
641    // ── union / tagged union ──────────────────────────────────────────────────
642
643    #[test]
644    fn zig_bare_union_parsed_as_union() {
645        let src = "const U = union { a: u8, b: u32 };";
646        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
647        assert_eq!(layouts.len(), 1);
648        let l = &layouts[0];
649        assert_eq!(l.name, "U");
650        assert!(l.is_union, "union should have is_union=true");
651    }
652
653    #[test]
654    fn zig_bare_union_total_size_is_max_field() {
655        // a: u8 (1B), b: u32 (4B) → max = 4B, aligned to 4
656        let src = "const U = union { a: u8, b: u32 };";
657        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
658        let l = &layouts[0];
659        assert_eq!(l.total_size, 4);
660    }
661
662    #[test]
663    fn zig_union_all_fields_at_offset_zero() {
664        let src = "const U = union { a: u8, b: u64 };";
665        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
666        let l = &layouts[0];
667        for field in &l.fields {
668            assert_eq!(
669                field.offset, 0,
670                "union field '{}' should be at offset 0",
671                field.name
672            );
673        }
674    }
675
676    #[test]
677    fn zig_tagged_union_has_tag_field() {
678        let src = "const T = union(enum) { ok: u32, err: void };";
679        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
680        let l = &layouts[0];
681        assert!(
682            l.fields.iter().any(|f| f.name == "__tag"),
683            "tagged union should have a synthetic __tag field"
684        );
685    }
686
687    #[test]
688    fn zig_tagged_union_size_includes_tag() {
689        // ok: u32 (4B), err: void (0B) → payload = 4B, tag = 1B (2 variants ≤ 256)
690        // total = (4 + 1).next_multiple_of(4) = 8B
691        let src = "const T = union(enum) { ok: u32, err: void };";
692        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
693        let l = &layouts[0];
694        // payload (4B) + tag (1B) → 5B → rounded to align 4 = 8B
695        assert_eq!(l.total_size, 8);
696    }
697
698    #[test]
699    fn zig_union_with_largest_field_u64() {
700        // a: u8 (1B), b: u64 (8B), c: u32 (4B) → max = 8B, align = 8
701        let src = "const U = union { a: u8, b: u64, c: u32 };";
702        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
703        let l = &layouts[0];
704        assert_eq!(l.total_size, 8);
705        assert_eq!(l.align, 8);
706    }
707
708    #[test]
709    fn zig_struct_and_union_in_same_file() {
710        let src = "const S = struct { x: u32 };\nconst U = union { a: u8, b: u32 };";
711        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
712        assert_eq!(layouts.len(), 2);
713        assert!(layouts.iter().any(|l| l.name == "S" && !l.is_union));
714        assert!(layouts.iter().any(|l| l.name == "U" && l.is_union));
715    }
716
717    // ── bad weather: unions ───────────────────────────────────────────────────
718
719    #[test]
720    fn zig_empty_union_returns_none() {
721        // Empty union body → no layout produced
722        let src = "const E = union {};";
723        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
724        assert!(layouts.is_empty(), "empty union should produce no layout");
725    }
726
727    #[test]
728    fn zig_union_no_padding_finding() {
729        // Unions should never report inter-field padding (all fields at offset 0)
730        let src = "const U = union { a: u8, b: u64 };";
731        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
732        let gaps = padlock_core::ir::find_padding(&layouts[0]);
733        assert!(
734            gaps.is_empty(),
735            "unions should have no padding gaps: {:?}",
736            gaps
737        );
738    }
739
740    // ── type-table tests ──────────────────────────────────────────────────────
741
742    #[test]
743    fn zig_c_interop_types_correct_size() {
744        assert_eq!(zig_type_size_align("c_char", &X86_64_SYSV), (1, 1));
745        assert_eq!(zig_type_size_align("c_short", &X86_64_SYSV), (2, 2));
746        assert_eq!(zig_type_size_align("c_ushort", &X86_64_SYSV), (2, 2));
747        assert_eq!(zig_type_size_align("c_int", &X86_64_SYSV), (4, 4));
748        assert_eq!(zig_type_size_align("c_uint", &X86_64_SYSV), (4, 4));
749        // c_long is pointer-sized on LP64 (Linux/macOS x86-64)
750        assert_eq!(zig_type_size_align("c_long", &X86_64_SYSV), (8, 8));
751        assert_eq!(zig_type_size_align("c_ulong", &X86_64_SYSV), (8, 8));
752        assert_eq!(zig_type_size_align("c_longlong", &X86_64_SYSV), (8, 8));
753        assert_eq!(zig_type_size_align("c_ulonglong", &X86_64_SYSV), (8, 8));
754        assert_eq!(zig_type_size_align("c_float", &X86_64_SYSV), (4, 4));
755        assert_eq!(zig_type_size_align("c_double", &X86_64_SYSV), (8, 8));
756        assert_eq!(zig_type_size_align("c_longdouble", &X86_64_SYSV), (16, 16));
757    }
758
759    #[test]
760    fn zig_arbitrary_width_integers() {
761        // u1 → 1B (ceil(1/8)=1), aligned to 1
762        assert_eq!(zig_type_size_align("u1", &X86_64_SYSV), (1, 1));
763        // u3 → 1B (ceil(3/8)=1)
764        assert_eq!(zig_type_size_align("u3", &X86_64_SYSV), (1, 1));
765        // u9 → 2B (ceil(9/8)=2), aligned to 2
766        assert_eq!(zig_type_size_align("u9", &X86_64_SYSV), (2, 2));
767        // u24 → 3B, aligned to 4 (next power-of-two)
768        assert_eq!(zig_type_size_align("u24", &X86_64_SYSV), (3, 4));
769        // u48 → 6B, aligned to 8
770        assert_eq!(zig_type_size_align("u48", &X86_64_SYSV), (6, 8));
771        // i7 → 1B, aligned to 1
772        assert_eq!(zig_type_size_align("i7", &X86_64_SYSV), (1, 1));
773        // i128 is already in the table; u129 hits the arbitrary-width path
774        assert_eq!(zig_type_size_align("u129", &X86_64_SYSV), (17, 8));
775    }
776
777    #[test]
778    fn zig_struct_with_c_interop_types() {
779        // A Zig extern struct using C interop types
780        let src = "const Header = extern struct { version: c_uint, length: c_ushort, flags: u8 };";
781        let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
782        assert_eq!(layouts.len(), 1);
783        let l = &layouts[0];
784        assert_eq!(l.fields[0].size, 4); // c_uint
785        assert_eq!(l.fields[1].size, 2); // c_ushort
786        assert_eq!(l.fields[2].size, 1); // u8
787    }
788}