padlock_source/frontends/
go.rs1use padlock_core::arch::ArchConfig;
7use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
8use tree_sitter::{Node, Parser};
9
10fn 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), ty if ty.starts_with("[]") => (arch.pointer_size * 3, arch.pointer_size), 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 "error" => (arch.pointer_size * 2, arch.pointer_size),
30 _ => (arch.pointer_size, arch.pointer_size),
31 }
32}
33
34fn 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 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 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 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
170pub fn extract_guard_from_go_comment(comment: &str) -> Option<String> {
177 let c = comment.trim();
178 let body = c.strip_prefix("//").map(str::trim)?;
180
181 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 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 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
208fn trailing_comment_on_line(source: &str, node: Node<'_>) -> Option<String> {
210 let end = node.end_byte();
213 if end >= source.len() {
214 return None;
215 }
216 let rest = &source[end..];
217 let line = rest.lines().next().unwrap_or("");
219 if let Some(pos) = line.find("//") {
221 Some(line[pos..].to_string())
222 } else {
223 None
224 }
225}
226
227fn collect_field_declarations(
228 source: &str,
229 node: Node<'_>,
230 out: &mut Vec<(String, String, Option<String>)>,
231) {
232 let mut field_names: Vec<String> = Vec::new();
234 let mut ty_text: Option<String> = None;
235
236 for i in 0..node.child_count() {
237 if let Some(child) = node.child(i) {
238 match child.kind() {
239 "field_identifier" => field_names.push(source[child.byte_range()].to_string()),
240 "type_identifier" | "pointer_type" | "qualified_type" | "slice_type"
241 | "map_type" | "channel_type" | "array_type" => {
242 ty_text = Some(source[child.byte_range()].trim().to_string());
243 }
244 _ => {}
245 }
246 }
247 }
248
249 if let Some(ty) = ty_text {
250 let guard =
252 trailing_comment_on_line(source, node).and_then(|c| extract_guard_from_go_comment(&c));
253 for name in field_names {
254 out.push((name, ty.clone(), guard.clone()));
255 }
256 }
257}
258
259pub fn parse_go(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
262 let mut parser = Parser::new();
263 parser.set_language(&tree_sitter_go::language())?;
264 let tree = parser
265 .parse(source, None)
266 .ok_or_else(|| anyhow::anyhow!("tree-sitter-go parse failed"))?;
267 Ok(extract_structs(source, tree.root_node(), arch))
268}
269
270#[cfg(test)]
273mod tests {
274 use super::*;
275 use padlock_core::arch::X86_64_SYSV;
276
277 #[test]
278 fn parse_simple_go_struct() {
279 let src = r#"
280package main
281type Point struct {
282 X int32
283 Y int32
284}
285"#;
286 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
287 assert_eq!(layouts.len(), 1);
288 assert_eq!(layouts[0].name, "Point");
289 assert_eq!(layouts[0].fields.len(), 2);
290 }
291
292 #[test]
293 fn go_layout_with_padding() {
294 let src = "package p\ntype T struct { A bool; B int64 }";
295 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
296 assert_eq!(layouts.len(), 1);
297 let l = &layouts[0];
298 assert_eq!(l.fields[0].offset, 0);
299 assert_eq!(l.fields[1].offset, 8); }
301
302 #[test]
303 fn go_string_is_two_words() {
304 let src = "package p\ntype S struct { Name string }";
305 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
306 assert_eq!(layouts[0].fields[0].size, 16); }
308
309 #[test]
312 fn extract_guard_padlock_form() {
313 assert_eq!(
314 extract_guard_from_go_comment("// padlock:guard=mu"),
315 Some("mu".to_string())
316 );
317 }
318
319 #[test]
320 fn extract_guard_guarded_by_form() {
321 assert_eq!(
322 extract_guard_from_go_comment("// guarded_by: counter_lock"),
323 Some("counter_lock".to_string())
324 );
325 }
326
327 #[test]
328 fn extract_guard_checklocksprotects_form() {
329 assert_eq!(
330 extract_guard_from_go_comment("// +checklocksprotects:mu"),
331 Some("mu".to_string())
332 );
333 }
334
335 #[test]
336 fn extract_guard_no_match_returns_none() {
337 assert!(extract_guard_from_go_comment("// just a comment").is_none());
338 assert!(extract_guard_from_go_comment("// TODO: fix this").is_none());
339 }
340
341 #[test]
342 fn go_struct_padlock_guard_annotation_sets_concurrent() {
343 let src = r#"package p
344type Cache struct {
345 Readers int64 // padlock:guard=mu
346 Writers int64 // padlock:guard=other_mu
347 Mu sync.Mutex
348}
349"#;
350 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
351 let l = &layouts[0];
352 if let AccessPattern::Concurrent { guard, .. } = &l.fields[0].access {
354 assert_eq!(guard.as_deref(), Some("mu"));
355 } else {
356 panic!(
357 "expected Concurrent for Readers, got {:?}",
358 l.fields[0].access
359 );
360 }
361 if let AccessPattern::Concurrent { guard, .. } = &l.fields[1].access {
362 assert_eq!(guard.as_deref(), Some("other_mu"));
363 } else {
364 panic!(
365 "expected Concurrent for Writers, got {:?}",
366 l.fields[1].access
367 );
368 }
369 }
370
371 #[test]
372 fn go_struct_different_guards_same_cache_line_is_false_sharing() {
373 let src = r#"package p
374type HotPath struct {
375 Readers int64 // padlock:guard=lock_a
376 Writers int64 // padlock:guard=lock_b
377}
378"#;
379 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
380 assert!(padlock_core::analysis::false_sharing::has_false_sharing(
381 &layouts[0]
382 ));
383 }
384
385 #[test]
386 fn go_struct_same_guard_is_not_false_sharing() {
387 let src = r#"package p
388type Safe struct {
389 A int64 // padlock:guard=mu
390 B int64 // padlock:guard=mu
391}
392"#;
393 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
394 assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
395 &layouts[0]
396 ));
397 }
398}