1mod canonicalize;
2
3pub(crate) use canonicalize::{
4 canonicalize_control_flow_targets, remove_temp_control_flow_target_labels,
5};
6use {
7 crate::{ast::AST, astnode::ASTNode},
8 sbpf_analyze::remove_dead_functions,
9 sbpf_ir::{Cfg, CfgRodata, InputNode, control_flow_graph},
10 std::collections::HashSet,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CfgDumpStage {
15 BeforeDfe,
16 AfterDfe,
17}
18
19impl CfgDumpStage {
20 pub fn file_name(self) -> &'static str {
21 match self {
22 Self::BeforeDfe => "dfe-before.dot",
23 Self::AfterDfe => "dfe-after.dot",
24 }
25 }
26}
27
28pub fn eliminate_unreachable_functions(ast: &mut AST) {
30 eliminate_unreachable_functions_with_observer(ast, |_, _| {});
31}
32
33pub fn eliminate_unreachable_functions_with_observer<F>(ast: &mut AST, mut observe: F)
36where
37 F: FnMut(CfgDumpStage, &Cfg),
38{
39 let mut cfg = cfg_for_ast(ast);
40 observe(CfgDumpStage::BeforeDfe, &cfg);
41
42 let removed_functions = remove_dead_functions(&mut cfg);
43
44 if !removed_functions.is_empty() {
45 let dead_node_ids: HashSet<usize> = removed_functions
46 .into_iter()
47 .flat_map(|f| f.node_ids)
48 .collect();
49 strip_dead_nodes(ast, &dead_node_ids);
50 }
51
52 assign_offsets(ast);
53
54 let cfg = cfg_for_ast(ast);
55 observe(CfgDumpStage::AfterDfe, &cfg);
56}
57
58fn strip_dead_nodes(ast: &mut AST, dead_node_ids: &HashSet<usize>) {
61 ast.nodes = std::mem::take(&mut ast.nodes)
62 .into_iter()
63 .enumerate()
64 .filter(|(idx, node)| {
65 !matches!(node, ASTNode::Label { .. } | ASTNode::Instruction { .. })
66 || !dead_node_ids.contains(idx)
67 })
68 .map(|(_, node)| node)
69 .collect();
70}
71
72pub fn assign_offsets(ast: &mut AST) {
76 let mut text_offset = 0u64;
77 let mut text_size = 0u64;
78 for node in &mut ast.nodes {
79 match node {
80 ASTNode::Label { offset, .. } => *offset = text_offset,
81 ASTNode::Instruction {
82 instruction,
83 offset,
84 } => {
85 *offset = text_offset;
86 let size = instruction.get_size();
87 text_offset += size;
88 text_size += size;
89 }
90 _ => {}
91 }
92 }
93 ast.set_text_size(text_size);
94}
95
96fn cfg_for_ast(ast: &AST) -> Cfg {
97 let function_entries = function_entries(ast);
98 let entry_label = ast.nodes.iter().find_map(|node| {
99 if let ASTNode::GlobalDecl { global_decl } = node {
100 Some(global_decl.entry_label.as_str())
101 } else {
102 None
103 }
104 });
105 let nodes = ast.nodes.iter().map(|node| match node {
106 ASTNode::Label { label, .. } => InputNode::Label(label.name.as_str()),
107 ASTNode::Instruction { instruction, .. } => InputNode::Instruction(instruction),
108 _ => InputNode::Other,
109 });
110 let mut cfg = control_flow_graph(nodes, &function_entries, entry_label);
111 if !ast.rodata_nodes.is_empty() {
112 let rodata = ast.rodata_nodes.iter().filter_map(|node| {
113 let ASTNode::ROData { rodata, offset } = node else {
114 return None;
115 };
116 Some(CfgRodata::new(&rodata.name, *offset, rodata.get_size()))
117 });
118 let references = ast
119 .rodata_relocations()
120 .iter()
121 .map(|relocation| (relocation.offset, relocation.target.as_str()));
122 cfg.set_rodata(rodata, references);
123 }
124 cfg
125}
126
127fn function_entries(ast: &AST) -> HashSet<String> {
128 ast.function_entries().clone()
129}
130
131#[cfg(test)]
132mod tests {
133 use {
134 super::*,
135 crate::{
136 astnode::{GlobalDecl, Label, ROData},
137 parser::Token,
138 },
139 either::Either,
140 sbpf_common::{
141 inst_param::{Number, Register},
142 instruction::Instruction,
143 opcode::Opcode,
144 },
145 };
146
147 #[test]
148 fn test_optimizer_preserves_unreachable_blocks_in_reachable_function() {
149 let mut ast = AST::new();
150 ast.add_function_entry("entrypoint".to_string());
151 ast.nodes = vec![
152 label_node("entrypoint", 0),
153 instruction_node(
154 Opcode::Ja,
155 None,
156 0,
157 Some(Either::Left("target".to_string())),
158 ),
159 instruction_node(Opcode::Mov64Imm, Some(0), 8, None),
160 label_node("target", 16),
161 instruction_node(Opcode::Exit, None, 16, None),
162 ];
163 ast.set_text_size(24);
164
165 eliminate_unreachable_functions(&mut ast);
166
167 assert_eq!(ast.nodes.len(), 5);
168 assert!(matches!(
169 &ast.nodes[1],
170 ASTNode::Instruction { instruction, offset }
171 if instruction.opcode == Opcode::Ja
172 && instruction.off == Some(Either::Left("target".to_string()))
173 && *offset == 0
174 ));
175 assert!(matches!(
176 &ast.nodes[2],
177 ASTNode::Instruction { instruction, offset }
178 if instruction.opcode == Opcode::Mov64Imm && *offset == 8
179 ));
180 assert!(matches!(
181 &ast.nodes[3],
182 ASTNode::Label { label, offset } if label.name == "target" && *offset == 16
183 ));
184 assert!(matches!(
185 &ast.nodes[4],
186 ASTNode::Instruction { instruction, offset }
187 if instruction.opcode == Opcode::Exit && *offset == 16
188 ));
189 }
190
191 #[test]
192 fn test_optimizer_removes_uncalled_function_only() {
193 let mut ast = AST::new();
194 ast.add_function_entry("entrypoint".to_string());
195 ast.add_function_entry("dead".to_string());
196 ast.add_function_entry("callee".to_string());
197 ast.nodes = vec![
198 label_node("entrypoint", 0),
199 call_node("callee", 0),
200 instruction_node(Opcode::Exit, None, 8, None),
201 label_node("dead", 16),
202 instruction_node(Opcode::Exit, None, 16, None),
203 label_node("callee", 24),
204 instruction_node(Opcode::Exit, None, 24, None),
205 ];
206 ast.set_text_size(32);
207
208 eliminate_unreachable_functions(&mut ast);
209
210 assert!(
211 !ast.nodes
212 .iter()
213 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "dead"))
214 );
215 assert!(
216 ast.nodes
217 .iter()
218 .any(|node| matches!(node, ASTNode::Label { label, offset }
219 if label.name == "callee" && *offset == 16))
220 );
221 assert_eq!(
222 ast.nodes
223 .iter()
224 .filter(|node| matches!(node, ASTNode::Instruction { .. }))
225 .count(),
226 3
227 );
228 }
229
230 #[test]
231 fn test_optimizer_uses_declared_entry_when_it_is_not_first() {
232 let mut ast = AST::new();
233 ast.add_function_entry("helper".to_string());
234 ast.add_function_entry("dead".to_string());
235 ast.add_function_entry("entrypoint".to_string());
236 ast.nodes = vec![
237 ASTNode::GlobalDecl {
238 global_decl: GlobalDecl {
239 entry_label: "entrypoint".to_string(),
240 span: 0..0,
241 },
242 },
243 label_node("helper", 0),
244 instruction_node(Opcode::Exit, None, 0, None),
245 label_node("dead", 8),
246 instruction_node(Opcode::Exit, None, 8, None),
247 label_node("entrypoint", 16),
248 call_node("helper", 16),
249 instruction_node(Opcode::Exit, None, 24, None),
250 ];
251 ast.set_text_size(32);
252
253 let cfg = cfg_for_ast(&ast);
254 assert_eq!(cfg.functions()[0].name(), "entrypoint");
255 assert_eq!(cfg.functions()[0].entry_block_id(), Some(2)); eliminate_unreachable_functions(&mut ast);
258
259 assert!(ast.nodes.iter().any(
260 |node| matches!(node, ASTNode::Label { label, .. } if label.name == "entrypoint")
261 ));
262 assert!(
263 ast.nodes
264 .iter()
265 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "helper"))
266 );
267 assert!(
268 !ast.nodes
269 .iter()
270 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "dead"))
271 );
272 }
273
274 #[test]
275 fn test_optimizer_preserves_function_targeted_by_rodata_relocation() {
276 let mut ast = AST::new();
277 ast.add_function_entry("entrypoint".to_string());
278 ast.add_function_entry("target".to_string());
279 ast.add_function_entry("dead".to_string());
280 ast.nodes = vec![
281 ASTNode::GlobalDecl {
282 global_decl: GlobalDecl {
283 entry_label: "entrypoint".to_string(),
284 span: 0..0,
285 },
286 },
287 label_node("entrypoint", 0),
288 instruction_node(Opcode::Exit, None, 0, None),
289 label_node("target", 8),
290 instruction_node(Opcode::Exit, None, 8, None),
291 label_node("dead", 16),
292 instruction_node(Opcode::Exit, None, 16, None),
293 ];
294 ast.rodata_nodes.push(ASTNode::ROData {
295 rodata: ROData {
296 name: "pointer".to_string(),
297 args: vec![
298 Token::Directive("quad".to_string(), 0..0),
299 Token::VectorLiteral(vec![Number::Int(0)], 0..0),
300 ],
301 span: 0..0,
302 },
303 offset: 0,
304 });
305 ast.set_text_size(24);
306 ast.set_rodata_size(8);
307 ast.add_rodata_relocation(0, "target".to_string());
308
309 eliminate_unreachable_functions(&mut ast);
310
311 assert!(ast.nodes.iter().any(
312 |node| matches!(node, ASTNode::Label { label, .. } if label.name == "entrypoint")
313 ));
314 assert!(
315 ast.nodes
316 .iter()
317 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "target"))
318 );
319 assert!(
320 !ast.nodes
321 .iter()
322 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "dead"))
323 );
324 }
325
326 #[test]
327 fn test_strip_dead_nodes_and_assign_offsets_recomputes_cumulatively() {
328 let mut ast = AST::new();
329 ast.nodes = vec![
331 label_node("entrypoint", 0),
332 instruction_node(Opcode::Exit, None, 0, None),
333 label_node("dead_a", 8),
334 instruction_node(Opcode::Exit, None, 8, None),
335 label_node("live", 16),
336 instruction_node(Opcode::Exit, None, 16, None),
337 label_node("dead_b", 24),
338 instruction_node(Opcode::Exit, None, 24, None),
339 label_node("target", 32),
340 instruction_node(Opcode::Exit, None, 32, None),
341 ];
342 ast.set_text_size(40);
343
344 strip_dead_nodes(&mut ast, &HashSet::from([2usize, 3, 6, 7]));
345 assign_offsets(&mut ast);
346
347 assert!(matches!(
348 &ast.nodes[2],
349 ASTNode::Label { label, offset } if label.name == "live" && *offset == 8
350 ));
351 assert!(matches!(
352 &ast.nodes[4],
353 ASTNode::Label { label, offset } if label.name == "target" && *offset == 16
354 ));
355 }
356
357 #[test]
358 fn test_assign_offsets_recomputes_from_scratch() {
359 let mut ast = AST::new();
360 ast.nodes = vec![
361 label_node("entrypoint", 999),
362 instruction_node(Opcode::Exit, None, 999, None),
363 label_node("next", 999),
364 instruction_node(Opcode::Exit, None, 999, None),
365 ];
366
367 assign_offsets(&mut ast);
368
369 assert!(matches!(
370 &ast.nodes[0],
371 ASTNode::Label { label, offset } if label.name == "entrypoint" && *offset == 0
372 ));
373 assert!(matches!(
374 &ast.nodes[1],
375 ASTNode::Instruction { offset, .. } if *offset == 0
376 ));
377 assert!(matches!(
378 &ast.nodes[2],
379 ASTNode::Label { label, offset } if label.name == "next" && *offset == 8
380 ));
381 }
382
383 fn label_node(name: &str, offset: u64) -> ASTNode {
384 ASTNode::Label {
385 label: Label {
386 name: name.to_string(),
387 span: 0..0,
388 },
389 offset,
390 }
391 }
392
393 fn instruction_node(
394 opcode: Opcode,
395 dst: Option<u8>,
396 offset: u64,
397 off: Option<Either<String, i16>>,
398 ) -> ASTNode {
399 ASTNode::Instruction {
400 instruction: Instruction {
401 opcode,
402 dst: dst.map(|n| Register { n }),
403 src: None,
404 off,
405 imm: None,
406 span: 0..0,
407 },
408 offset,
409 }
410 }
411
412 fn call_node(target: &str, offset: u64) -> ASTNode {
413 ASTNode::Instruction {
414 instruction: Instruction {
415 opcode: Opcode::Call,
416 dst: None,
417 src: None,
418 off: None,
419 imm: Some(Either::Left(target.to_string())),
420 span: 0..0,
421 },
422 offset,
423 }
424 }
425}