Skip to main content

padlock_source/frontends/
go.rs

1// padlock-source/src/frontends/go.rs
2//
3// Extracts struct layouts from Go source using tree-sitter-go.
4// Sizes use Go's platform-native alignment rules (same as C on the target arch).
5
6use padlock_core::arch::ArchConfig;
7use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
8use tree_sitter::{Node, Parser};
9
10// ── type resolution ───────────────────────────────────────────────────────────
11
12fn go_type_size_align(ty: &str, arch: &'static ArchConfig) -> (usize, usize) {
13    match ty.trim() {
14        "bool" => (1, 1),
15        "int8" | "uint8" | "byte" => (1, 1),
16        "int16" | "uint16" => (2, 2),
17        "int32" | "uint32" | "rune" | "float32" => (4, 4),
18        "int64" | "uint64" | "float64" | "complex64" => (8, 8),
19        "complex128" => (16, 16),
20        "int" | "uint" => (arch.pointer_size, arch.pointer_size),
21        "uintptr" => (arch.pointer_size, arch.pointer_size),
22        "string" => (arch.pointer_size * 2, arch.pointer_size), // ptr + len
23        ty if ty.starts_with("[]") => (arch.pointer_size * 3, arch.pointer_size), // ptr+len+cap
24        ty if ty.starts_with("map[") || ty.starts_with("chan ") => {
25            (arch.pointer_size, arch.pointer_size)
26        }
27        ty if ty.starts_with('*') => (arch.pointer_size, arch.pointer_size),
28        // Interface types: two-word fat pointer
29        "error" => (arch.pointer_size * 2, arch.pointer_size),
30        _ => (arch.pointer_size, arch.pointer_size),
31    }
32}
33
34// ── tree-sitter walker ────────────────────────────────────────────────────────
35
36fn extract_structs(source: &str, root: Node<'_>, arch: &'static ArchConfig) -> Vec<StructLayout> {
37    let mut layouts = Vec::new();
38    let mut stack = vec![root];
39
40    while let Some(node) = stack.pop() {
41        for i in (0..node.child_count()).rev() {
42            if let Some(c) = node.child(i) {
43                stack.push(c);
44            }
45        }
46
47        // type_declaration → type_spec → struct_type
48        if node.kind() == "type_declaration" {
49            if let Some(layout) = parse_type_declaration(source, node, arch) {
50                layouts.push(layout);
51            }
52        }
53    }
54    layouts
55}
56
57fn parse_type_declaration(
58    source: &str,
59    node: Node<'_>,
60    arch: &'static ArchConfig,
61) -> Option<StructLayout> {
62    // type_declaration has a type_spec child
63    for i in 0..node.child_count() {
64        let child = node.child(i)?;
65        if child.kind() == "type_spec" {
66            return parse_type_spec(source, child, arch);
67        }
68    }
69    None
70}
71
72fn parse_type_spec(
73    source: &str,
74    node: Node<'_>,
75    arch: &'static ArchConfig,
76) -> Option<StructLayout> {
77    let mut name: Option<String> = None;
78    let mut struct_node: Option<Node> = None;
79
80    for i in 0..node.child_count() {
81        let child = node.child(i)?;
82        match child.kind() {
83            "type_identifier" => name = Some(source[child.byte_range()].to_string()),
84            "struct_type" => struct_node = Some(child),
85            _ => {}
86        }
87    }
88
89    let name = name?;
90    let struct_node = struct_node?;
91    parse_struct_type(source, struct_node, name, arch)
92}
93
94fn parse_struct_type(
95    source: &str,
96    node: Node<'_>,
97    name: String,
98    arch: &'static ArchConfig,
99) -> Option<StructLayout> {
100    let mut raw_fields: Vec<(String, String, Option<String>)> = Vec::new();
101
102    for i in 0..node.child_count() {
103        let child = node.child(i)?;
104        if child.kind() == "field_declaration_list" {
105            for j in 0..child.child_count() {
106                let field_node = child.child(j)?;
107                if field_node.kind() == "field_declaration" {
108                    collect_field_declarations(source, field_node, &mut raw_fields);
109                }
110            }
111        }
112    }
113
114    if raw_fields.is_empty() {
115        return None;
116    }
117
118    // Simulate layout
119    let mut offset = 0usize;
120    let mut struct_align = 1usize;
121    let mut fields: Vec<Field> = Vec::new();
122
123    for (fname, ty_name, guard) in raw_fields {
124        let (size, align) = go_type_size_align(&ty_name, arch);
125        if align > 0 {
126            offset = offset.next_multiple_of(align);
127        }
128        struct_align = struct_align.max(align);
129        let access = if let Some(g) = guard {
130            AccessPattern::Concurrent {
131                guard: Some(g),
132                is_atomic: false,
133            }
134        } else {
135            AccessPattern::Unknown
136        };
137        fields.push(Field {
138            name: fname,
139            ty: TypeInfo::Primitive {
140                name: ty_name,
141                size,
142                align,
143            },
144            offset,
145            size,
146            align,
147            source_file: None,
148            source_line: None,
149            access,
150        });
151        offset += size;
152    }
153    if struct_align > 0 {
154        offset = offset.next_multiple_of(struct_align);
155    }
156
157    Some(StructLayout {
158        name,
159        total_size: offset,
160        align: struct_align,
161        fields,
162        source_file: None,
163        source_line: None,
164        arch,
165        is_packed: false,
166        is_union: false,
167    })
168}
169
170/// Extract a guard name from a Go field's trailing line comment.
171///
172/// Recognised forms (must appear after the field type on the same line):
173/// - `// padlock:guard=mu`
174/// - `// guarded_by: mu`
175/// - `// +checklocksprotects:mu` (gVisor-style)
176pub fn extract_guard_from_go_comment(comment: &str) -> Option<String> {
177    let c = comment.trim();
178    // Strip leading `//` and optional whitespace
179    let body = c.strip_prefix("//").map(str::trim)?;
180
181    // padlock:guard=mu
182    if let Some(rest) = body.strip_prefix("padlock:guard=") {
183        let guard = rest.trim();
184        if !guard.is_empty() {
185            return Some(guard.to_string());
186        }
187    }
188    // guarded_by: mu
189    if let Some(rest) = body
190        .strip_prefix("guarded_by:")
191        .or_else(|| body.strip_prefix("guarded_by ="))
192    {
193        let guard = rest.trim();
194        if !guard.is_empty() {
195            return Some(guard.to_string());
196        }
197    }
198    // +checklocksprotects:mu (gVisor)
199    if let Some(rest) = body.strip_prefix("+checklocksprotects:") {
200        let guard = rest.trim();
201        if !guard.is_empty() {
202            return Some(guard.to_string());
203        }
204    }
205    None
206}
207
208/// Find the trailing line comment on the same source line as `node`.
209fn trailing_comment_on_line(source: &str, node: Node<'_>) -> Option<String> {
210    // The node's end byte is just past the last token on the field line.
211    // Read the rest of that line from the source.
212    let end = node.end_byte();
213    if end >= source.len() {
214        return None;
215    }
216    let rest = &source[end..];
217    // Take only up to the next newline
218    let line = rest.lines().next().unwrap_or("");
219    // Look for `//` in that remainder
220    line.find("//").map(|pos| line[pos..].to_string())
221}
222
223fn collect_field_declarations(
224    source: &str,
225    node: Node<'_>,
226    out: &mut Vec<(String, String, Option<String>)>,
227) {
228    // field_declaration: field_identifier+ type [comment]
229    let mut field_names: Vec<String> = Vec::new();
230    let mut ty_text: Option<String> = None;
231
232    for i in 0..node.child_count() {
233        if let Some(child) = node.child(i) {
234            match child.kind() {
235                "field_identifier" => field_names.push(source[child.byte_range()].to_string()),
236                "type_identifier" | "pointer_type" | "qualified_type" | "slice_type"
237                | "map_type" | "channel_type" | "array_type" => {
238                    ty_text = Some(source[child.byte_range()].trim().to_string());
239                }
240                _ => {}
241            }
242        }
243    }
244
245    if let Some(ty) = ty_text {
246        // Check for trailing guard comment on this field's line
247        let guard =
248            trailing_comment_on_line(source, node).and_then(|c| extract_guard_from_go_comment(&c));
249        for name in field_names {
250            out.push((name, ty.clone(), guard.clone()));
251        }
252    }
253}
254
255// ── public API ────────────────────────────────────────────────────────────────
256
257pub fn parse_go(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
258    let mut parser = Parser::new();
259    parser.set_language(&tree_sitter_go::language())?;
260    let tree = parser
261        .parse(source, None)
262        .ok_or_else(|| anyhow::anyhow!("tree-sitter-go parse failed"))?;
263    Ok(extract_structs(source, tree.root_node(), arch))
264}
265
266// ── tests ─────────────────────────────────────────────────────────────────────
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use padlock_core::arch::X86_64_SYSV;
272
273    #[test]
274    fn parse_simple_go_struct() {
275        let src = r#"
276package main
277type Point struct {
278    X int32
279    Y int32
280}
281"#;
282        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
283        assert_eq!(layouts.len(), 1);
284        assert_eq!(layouts[0].name, "Point");
285        assert_eq!(layouts[0].fields.len(), 2);
286    }
287
288    #[test]
289    fn go_layout_with_padding() {
290        let src = "package p\ntype T struct { A bool; B int64 }";
291        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
292        assert_eq!(layouts.len(), 1);
293        let l = &layouts[0];
294        assert_eq!(l.fields[0].offset, 0);
295        assert_eq!(l.fields[1].offset, 8); // bool (1) + 7 pad → 8
296    }
297
298    #[test]
299    fn go_string_is_two_words() {
300        let src = "package p\ntype S struct { Name string }";
301        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
302        assert_eq!(layouts[0].fields[0].size, 16); // ptr + len
303    }
304
305    // ── Go guard comment extraction ────────────────────────────────────────────
306
307    #[test]
308    fn extract_guard_padlock_form() {
309        assert_eq!(
310            extract_guard_from_go_comment("// padlock:guard=mu"),
311            Some("mu".to_string())
312        );
313    }
314
315    #[test]
316    fn extract_guard_guarded_by_form() {
317        assert_eq!(
318            extract_guard_from_go_comment("// guarded_by: counter_lock"),
319            Some("counter_lock".to_string())
320        );
321    }
322
323    #[test]
324    fn extract_guard_checklocksprotects_form() {
325        assert_eq!(
326            extract_guard_from_go_comment("// +checklocksprotects:mu"),
327            Some("mu".to_string())
328        );
329    }
330
331    #[test]
332    fn extract_guard_no_match_returns_none() {
333        assert!(extract_guard_from_go_comment("// just a comment").is_none());
334        assert!(extract_guard_from_go_comment("// TODO: fix this").is_none());
335    }
336
337    #[test]
338    fn go_struct_padlock_guard_annotation_sets_concurrent() {
339        let src = r#"package p
340type Cache struct {
341    Readers int64 // padlock:guard=mu
342    Writers int64 // padlock:guard=other_mu
343    Mu      sync.Mutex
344}
345"#;
346        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
347        let l = &layouts[0];
348        // Readers and Writers should be Concurrent with different guards
349        if let AccessPattern::Concurrent { guard, .. } = &l.fields[0].access {
350            assert_eq!(guard.as_deref(), Some("mu"));
351        } else {
352            panic!(
353                "expected Concurrent for Readers, got {:?}",
354                l.fields[0].access
355            );
356        }
357        if let AccessPattern::Concurrent { guard, .. } = &l.fields[1].access {
358            assert_eq!(guard.as_deref(), Some("other_mu"));
359        } else {
360            panic!(
361                "expected Concurrent for Writers, got {:?}",
362                l.fields[1].access
363            );
364        }
365    }
366
367    #[test]
368    fn go_struct_different_guards_same_cache_line_is_false_sharing() {
369        let src = r#"package p
370type HotPath struct {
371    Readers int64 // padlock:guard=lock_a
372    Writers int64 // padlock:guard=lock_b
373}
374"#;
375        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
376        assert!(padlock_core::analysis::false_sharing::has_false_sharing(
377            &layouts[0]
378        ));
379    }
380
381    #[test]
382    fn go_struct_same_guard_is_not_false_sharing() {
383        let src = r#"package p
384type Safe struct {
385    A int64 // padlock:guard=mu
386    B int64 // padlock:guard=mu
387}
388"#;
389        let layouts = parse_go(src, &X86_64_SYSV).unwrap();
390        assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
391            &layouts[0]
392        ));
393    }
394}