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