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