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_common::instruction::Instruction,
10 sbpf_ir::{Cfg, CfgRodata, InputNode, control_flow_graph},
11 std::collections::{HashMap, HashSet},
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum CfgDumpStage {
16 BeforeDfe,
17 AfterDfe,
18}
19
20impl CfgDumpStage {
21 pub fn file_name(self) -> &'static str {
22 match self {
23 Self::BeforeDfe => "dfe-before.dot",
24 Self::AfterDfe => "dfe-after.dot",
25 }
26 }
27}
28
29pub fn optimize(ast: &mut AST) {
33 optimize_with_observer(ast, |_, _| {});
34}
35
36pub fn optimize_with_observer<F>(ast: &mut AST, mut observe: F)
40where
41 F: FnMut(CfgDumpStage, &Cfg),
42{
43 let mut cfg = cfg_for_ast(ast);
44 observe(CfgDumpStage::BeforeDfe, &cfg);
45 remove_dead_functions(&mut cfg);
46 observe(CfgDumpStage::AfterDfe, &cfg);
47 sbpf_analyze::fix_alu32_extension(&mut cfg);
48 write_cfg_to_ast(ast, &cfg);
49}
50
51pub fn eliminate_unreachable_functions(ast: &mut AST) {
53 let mut cfg = cfg_for_ast(ast);
54 remove_dead_functions(&mut cfg);
55 write_cfg_to_ast(ast, &cfg);
56}
57
58pub fn fix_alu32_extension(ast: &mut AST) {
62 let mut cfg = cfg_for_ast(ast);
63 sbpf_analyze::fix_alu32_extension(&mut cfg);
64 write_cfg_to_ast(ast, &cfg);
65}
66
67pub(crate) fn cfg_for_ast(ast: &AST) -> Cfg {
70 let function_entries = function_entries(ast);
71 let entry_label = ast.nodes.iter().find_map(|node| {
72 if let ASTNode::GlobalDecl { global_decl } = node {
73 Some(global_decl.entry_label.as_str())
74 } else {
75 None
76 }
77 });
78 let nodes = ast.nodes.iter().map(|node| match node {
79 ASTNode::Label { label, .. } => InputNode::Label(label.name.as_str()),
80 ASTNode::Instruction { instruction, .. } => InputNode::Instruction(instruction),
81 _ => InputNode::Other,
82 });
83 let mut cfg = control_flow_graph(nodes, &function_entries, entry_label);
84 if !ast.rodata_nodes.is_empty() {
85 let rodata = ast.rodata_nodes.iter().filter_map(|node| {
86 let ASTNode::ROData { rodata, offset } = node else {
87 return None;
88 };
89 Some(CfgRodata::new(&rodata.name, *offset, rodata.get_size()))
90 });
91 let references = ast
92 .rodata_relocations()
93 .iter()
94 .map(|relocation| (relocation.offset, relocation.target.as_str()));
95 cfg.set_rodata(rodata, references);
96 }
97 cfg
98}
99
100pub(crate) fn write_cfg_to_ast(ast: &mut AST, cfg: &Cfg) {
108 let live: HashSet<usize> = cfg
109 .all_blocks()
110 .flat_map(|(_, block)| block.node_ids().iter().copied())
111 .collect();
112
113 let mut rewritten: HashMap<usize, Vec<Instruction>> = HashMap::new();
114 for (_, block) in cfg.all_blocks() {
115 let mut last_source = None;
116 for node in block.instructions() {
117 let Some(instruction) = node.instruction() else {
118 continue;
119 };
120 match node.source_node_id() {
121 Some(id) => {
122 rewritten.insert(id, vec![instruction.clone()]);
123 last_source = Some(id);
124 }
125 None => {
126 let id = last_source
127 .expect("inserted instructions follow a source instruction in their block");
128 rewritten
129 .get_mut(&id)
130 .expect("source instruction was recorded")
131 .push(instruction.clone());
132 }
133 }
134 }
135 }
136
137 ast.nodes = std::mem::take(&mut ast.nodes)
138 .into_iter()
139 .enumerate()
140 .flat_map(|(idx, node)| match node {
141 ASTNode::Label { .. } if !live.contains(&idx) => vec![],
142 ASTNode::Instruction { .. } => rewritten
143 .remove(&idx)
144 .unwrap_or_default()
145 .into_iter()
146 .map(|instruction| ASTNode::Instruction {
147 instruction,
148 offset: 0,
149 })
150 .collect(),
151 node => vec![node],
152 })
153 .collect();
154
155 assign_offsets(ast);
156}
157
158pub fn assign_offsets(ast: &mut AST) {
162 let mut text_offset = 0u64;
163 let mut text_size = 0u64;
164 for node in &mut ast.nodes {
165 match node {
166 ASTNode::Label { offset, .. } => *offset = text_offset,
167 ASTNode::Instruction {
168 instruction,
169 offset,
170 } => {
171 *offset = text_offset;
172 let size = instruction.get_size();
173 text_offset += size;
174 text_size += size;
175 }
176 _ => {}
177 }
178 }
179 ast.set_text_size(text_size);
180}
181
182fn function_entries(ast: &AST) -> HashSet<String> {
183 ast.function_entries().clone()
184}
185
186#[cfg(test)]
187mod tests {
188 use {
189 super::*,
190 crate::{
191 SbpfArch,
192 ast::{OptimizationConfig, build_program},
193 astnode::{GlobalDecl, Label, ROData},
194 parser::Token,
195 },
196 either::Either,
197 sbpf_common::{
198 inst_param::{Number, Register},
199 instruction::Instruction,
200 opcode::Opcode,
201 },
202 };
203
204 #[test]
205 fn test_optimizer_preserves_unreachable_blocks_in_reachable_function() {
206 let mut ast = AST::new();
207 ast.add_function_entry("entrypoint".to_string());
208 ast.nodes = vec![
209 label_node("entrypoint", 0),
210 instruction_node(
211 Opcode::Ja,
212 None,
213 0,
214 Some(Either::Left("target".to_string())),
215 ),
216 instruction_node(Opcode::Mov64Imm, Some(0), 8, None),
217 label_node("target", 16),
218 instruction_node(Opcode::Exit, None, 16, None),
219 ];
220 ast.set_text_size(24);
221
222 eliminate_unreachable_functions(&mut ast);
223
224 assert_eq!(ast.nodes.len(), 5);
225 assert!(matches!(
226 &ast.nodes[1],
227 ASTNode::Instruction { instruction, offset }
228 if instruction.opcode == Opcode::Ja
229 && instruction.off == Some(Either::Left("target".to_string()))
230 && *offset == 0
231 ));
232 assert!(matches!(
233 &ast.nodes[2],
234 ASTNode::Instruction { instruction, offset }
235 if instruction.opcode == Opcode::Mov64Imm && *offset == 8
236 ));
237 assert!(matches!(
238 &ast.nodes[3],
239 ASTNode::Label { label, offset } if label.name == "target" && *offset == 16
240 ));
241 assert!(matches!(
242 &ast.nodes[4],
243 ASTNode::Instruction { instruction, offset }
244 if instruction.opcode == Opcode::Exit && *offset == 16
245 ));
246 }
247
248 #[test]
249 fn test_optimizer_removes_uncalled_function_only() {
250 let mut ast = AST::new();
251 ast.add_function_entry("entrypoint".to_string());
252 ast.add_function_entry("dead".to_string());
253 ast.add_function_entry("callee".to_string());
254 ast.nodes = vec![
255 label_node("entrypoint", 0),
256 call_node("callee", 0),
257 instruction_node(Opcode::Exit, None, 8, None),
258 label_node("dead", 16),
259 instruction_node(Opcode::Exit, None, 16, None),
260 label_node("callee", 24),
261 instruction_node(Opcode::Exit, None, 24, None),
262 ];
263 ast.set_text_size(32);
264
265 eliminate_unreachable_functions(&mut ast);
266
267 assert!(
268 !ast.nodes
269 .iter()
270 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "dead"))
271 );
272 assert!(
273 ast.nodes
274 .iter()
275 .any(|node| matches!(node, ASTNode::Label { label, offset }
276 if label.name == "callee" && *offset == 16))
277 );
278 assert_eq!(
279 ast.nodes
280 .iter()
281 .filter(|node| matches!(node, ASTNode::Instruction { .. }))
282 .count(),
283 3
284 );
285 }
286
287 #[test]
288 fn test_optimizer_uses_declared_entry_when_it_is_not_first() {
289 let mut ast = AST::new();
290 ast.add_function_entry("helper".to_string());
291 ast.add_function_entry("dead".to_string());
292 ast.add_function_entry("entrypoint".to_string());
293 ast.nodes = vec![
294 ASTNode::GlobalDecl {
295 global_decl: GlobalDecl {
296 entry_label: "entrypoint".to_string(),
297 span: 0..0,
298 },
299 },
300 label_node("helper", 0),
301 instruction_node(Opcode::Exit, None, 0, None),
302 label_node("dead", 8),
303 instruction_node(Opcode::Exit, None, 8, None),
304 label_node("entrypoint", 16),
305 call_node("helper", 16),
306 instruction_node(Opcode::Exit, None, 24, None),
307 ];
308 ast.set_text_size(32);
309
310 let cfg = cfg_for_ast(&ast);
311 assert_eq!(cfg.functions()[0].name(), "entrypoint");
312 assert_eq!(cfg.functions()[0].entry_block_id(), Some(2)); eliminate_unreachable_functions(&mut ast);
315
316 assert!(ast.nodes.iter().any(
317 |node| matches!(node, ASTNode::Label { label, .. } if label.name == "entrypoint")
318 ));
319 assert!(
320 ast.nodes
321 .iter()
322 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "helper"))
323 );
324 assert!(
325 !ast.nodes
326 .iter()
327 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "dead"))
328 );
329 }
330
331 #[test]
332 fn test_optimizer_preserves_function_targeted_by_rodata_relocation() {
333 let mut ast = AST::new();
334 ast.add_function_entry("entrypoint".to_string());
335 ast.add_function_entry("target".to_string());
336 ast.add_function_entry("dead".to_string());
337 ast.nodes = vec![
338 ASTNode::GlobalDecl {
339 global_decl: GlobalDecl {
340 entry_label: "entrypoint".to_string(),
341 span: 0..0,
342 },
343 },
344 label_node("entrypoint", 0),
345 instruction_node(Opcode::Exit, None, 0, None),
346 label_node("target", 8),
347 instruction_node(Opcode::Exit, None, 8, None),
348 label_node("dead", 16),
349 instruction_node(Opcode::Exit, None, 16, None),
350 ];
351 ast.rodata_nodes.push(ASTNode::ROData {
352 rodata: ROData {
353 name: "pointer".to_string(),
354 args: vec![
355 Token::Directive("quad".to_string(), 0..0),
356 Token::VectorLiteral(vec![Number::Int(0)], 0..0),
357 ],
358 span: 0..0,
359 },
360 offset: 0,
361 });
362 ast.set_text_size(24);
363 ast.set_rodata_size(8);
364 ast.add_rodata_relocation(0, "target".to_string());
365
366 eliminate_unreachable_functions(&mut ast);
367
368 assert!(ast.nodes.iter().any(
369 |node| matches!(node, ASTNode::Label { label, .. } if label.name == "entrypoint")
370 ));
371 assert!(
372 ast.nodes
373 .iter()
374 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "target"))
375 );
376 assert!(
377 !ast.nodes
378 .iter()
379 .any(|node| matches!(node, ASTNode::Label { label, .. } if label.name == "dead"))
380 );
381 }
382
383 #[test]
384 fn test_write_back_drops_dead_functions_and_recomputes_offsets_cumulatively() {
385 let mut ast = AST::new();
386 for name in ["entrypoint", "dead_a", "live", "dead_b", "target"] {
387 ast.add_function_entry(name.to_string());
388 }
389 ast.nodes = vec![
390 label_node("entrypoint", 0),
391 call_node("live", 0),
392 call_node("target", 8),
393 instruction_node(Opcode::Exit, None, 16, None),
394 label_node("dead_a", 24),
395 instruction_node(Opcode::Exit, None, 24, None),
396 label_node("live", 32),
397 instruction_node(Opcode::Exit, None, 32, None),
398 label_node("dead_b", 40),
399 instruction_node(Opcode::Exit, None, 40, None),
400 label_node("target", 48),
401 instruction_node(Opcode::Exit, None, 48, None),
402 ];
403 ast.set_text_size(56);
404
405 eliminate_unreachable_functions(&mut ast);
406
407 assert_eq!(
408 listing(&ast.nodes),
409 [
410 ("entrypoint:".to_string(), 0),
411 ("call".to_string(), 0),
412 ("call".to_string(), 8),
413 ("exit".to_string(), 16),
414 ("live:".to_string(), 24),
415 ("exit".to_string(), 24),
416 ("target:".to_string(), 32),
417 ("exit".to_string(), 32),
418 ]
419 );
420 }
421
422 #[test]
423 fn test_assign_offsets_recomputes_from_scratch() {
424 let mut ast = AST::new();
425 ast.nodes = vec![
426 label_node("entrypoint", 999),
427 instruction_node(Opcode::Exit, None, 999, None),
428 label_node("next", 999),
429 instruction_node(Opcode::Exit, None, 999, None),
430 ];
431
432 assign_offsets(&mut ast);
433
434 assert!(matches!(
435 &ast.nodes[0],
436 ASTNode::Label { label, offset } if label.name == "entrypoint" && *offset == 0
437 ));
438 assert!(matches!(
439 &ast.nodes[1],
440 ASTNode::Instruction { offset, .. } if *offset == 0
441 ));
442 assert!(matches!(
443 &ast.nodes[2],
444 ASTNode::Label { label, offset } if label.name == "next" && *offset == 8
445 ));
446 }
447
448 fn alu_node(opcode: Opcode, dst: u8, src: Option<u8>, imm: Option<i64>) -> ASTNode {
449 ASTNode::Instruction {
450 instruction: Instruction {
451 opcode,
452 dst: Some(Register { n: dst }),
453 src: src.map(|n| Register { n }),
454 off: None,
455 imm: imm.map(|v| Either::Right(Number::Int(v))),
456 span: 0..0,
457 },
458 offset: 0,
459 }
460 }
461
462 fn alu32_program() -> AST {
465 let mut ast = AST::new();
466 ast.add_function_entry("entrypoint".to_string());
467 ast.nodes = vec![
468 label_node("entrypoint", 0),
469 alu_node(Opcode::Add32Reg, 1, Some(2), None),
470 instruction_node(Opcode::Ja, None, 8, Some(Either::Left("done".to_string()))),
471 label_node("done", 16),
472 alu_node(Opcode::Lsh64Imm, 1, None, Some(1)),
473 alu_node(Opcode::Mov64Reg, 0, Some(1), None),
474 instruction_node(Opcode::Exit, None, 32, None),
475 ];
476 ast.set_text_size(40);
477 ast
478 }
479
480 fn listing(nodes: &[ASTNode]) -> Vec<(String, u64)> {
481 nodes
482 .iter()
483 .filter_map(|node| match node {
484 ASTNode::Label { label, offset } => Some((format!("{}:", label.name), *offset)),
485 ASTNode::Instruction {
486 instruction,
487 offset,
488 } => Some((instruction.opcode.to_string(), *offset)),
489 _ => None,
490 })
491 .collect()
492 }
493
494 #[test]
495 fn test_fix_alu32_extension_inserts_fixup_and_reassigns_offsets() {
496 let mut ast = alu32_program();
497
498 fix_alu32_extension(&mut ast);
499
500 assert_eq!(
501 listing(&ast.nodes),
502 [
503 ("entrypoint:".to_string(), 0),
504 ("add32".to_string(), 0),
505 ("mov32".to_string(), 8),
506 ("ja".to_string(), 16),
507 ("done:".to_string(), 24),
508 ("lsh64".to_string(), 24),
509 ("mov64".to_string(), 32),
510 ("exit".to_string(), 40),
511 ]
512 );
513 let ASTNode::Instruction { instruction, .. } = &ast.nodes[2] else {
514 panic!("expected the inserted instruction");
515 };
516 assert_eq!(instruction.opcode, Opcode::Mov32Reg);
517 assert_eq!(instruction.dst, Some(Register { n: 1 }));
518 assert_eq!(instruction.src, Some(Register { n: 1 }));
519 }
520
521 #[test]
522 fn test_fix_alu32_extension_narrows_copy_in_place() {
523 let mut ast = AST::new();
524 ast.add_function_entry("entrypoint".to_string());
525 ast.nodes = vec![
526 label_node("entrypoint", 0),
527 alu_node(Opcode::Add32Reg, 1, Some(2), None),
528 alu_node(Opcode::Mov64Reg, 0, Some(1), None),
529 instruction_node(Opcode::Exit, None, 16, None),
530 ];
531 ast.set_text_size(24);
532
533 fix_alu32_extension(&mut ast);
534
535 assert_eq!(
536 listing(&ast.nodes),
537 [
538 ("entrypoint:".to_string(), 0),
539 ("add32".to_string(), 0),
540 ("mov32".to_string(), 8),
541 ("exit".to_string(), 16),
542 ]
543 );
544 let ASTNode::Instruction { instruction, .. } = &ast.nodes[2] else {
545 panic!("expected the narrowed copy");
546 };
547 assert_eq!(instruction.opcode, Opcode::Mov32Reg);
548 assert_eq!(instruction.dst, Some(Register { n: 0 }));
549 assert_eq!(instruction.src, Some(Register { n: 1 }));
550 }
551
552 #[test]
553 fn test_fix_alu32_extension_promotes_in_place() {
554 let mut ast = AST::new();
555 ast.add_function_entry("entrypoint".to_string());
556 ast.nodes = vec![
557 label_node("entrypoint", 0),
558 alu_node(Opcode::Lddw, 1, None, Some(0x9000_0000)),
559 alu_node(Opcode::Sub32Imm, 1, None, Some(1)),
560 alu_node(Opcode::Mov64Reg, 0, Some(1), None),
561 instruction_node(Opcode::Exit, None, 32, None),
562 ];
563 ast.set_text_size(40);
564
565 fix_alu32_extension(&mut ast);
566
567 assert_eq!(
568 listing(&ast.nodes),
569 [
570 ("entrypoint:".to_string(), 0),
571 ("lddw".to_string(), 0),
572 ("sub64".to_string(), 16),
573 ("mov64".to_string(), 24),
574 ("exit".to_string(), 32),
575 ]
576 );
577 }
578
579 #[test]
580 fn test_fix_alu32_extension_leaves_unaffected_program_untouched() {
581 let mut ast = AST::new();
582 ast.add_function_entry("entrypoint".to_string());
583 ast.nodes = vec![
584 label_node("entrypoint", 0),
585 alu_node(Opcode::Add32Reg, 1, Some(2), None),
586 alu_node(Opcode::Mov32Reg, 0, Some(1), None),
587 instruction_node(Opcode::Exit, None, 16, None),
588 ];
589 assign_offsets(&mut ast);
590 let before = listing(&ast.nodes);
591
592 fix_alu32_extension(&mut ast);
593
594 assert_eq!(listing(&ast.nodes), before);
595 }
596
597 #[test]
598 fn test_build_program_runs_fixup_only_when_optimizing() {
599 for arch in [SbpfArch::V0, SbpfArch::V3] {
600 let disabled =
601 build_program(alu32_program(), arch, OptimizationConfig::disabled()).unwrap();
602 assert!(
603 !listing(disabled.code_section.get_nodes())
604 .iter()
605 .any(|(op, _)| op == "mov32"),
606 "{arch:?}: disabled must not rewrite"
607 );
608
609 let enabled =
610 build_program(alu32_program(), arch, OptimizationConfig::enabled()).unwrap();
611 let ops: Vec<String> = listing(enabled.code_section.get_nodes())
612 .into_iter()
613 .map(|(op, _)| op)
614 .collect();
615 assert_eq!(
616 ops,
617 [
618 "entrypoint:",
619 "add32",
620 "mov32",
621 "ja",
622 "done:",
623 "lsh64",
624 "mov64",
625 "exit"
626 ],
627 "{arch:?}"
628 );
629 }
630 }
631
632 fn label_node(name: &str, offset: u64) -> ASTNode {
633 ASTNode::Label {
634 label: Label {
635 name: name.to_string(),
636 span: 0..0,
637 },
638 offset,
639 }
640 }
641
642 fn instruction_node(
643 opcode: Opcode,
644 dst: Option<u8>,
645 offset: u64,
646 off: Option<Either<String, i16>>,
647 ) -> ASTNode {
648 ASTNode::Instruction {
649 instruction: Instruction {
650 opcode,
651 dst: dst.map(|n| Register { n }),
652 src: None,
653 off,
654 imm: None,
655 span: 0..0,
656 },
657 offset,
658 }
659 }
660
661 fn call_node(target: &str, offset: u64) -> ASTNode {
662 ASTNode::Instruction {
663 instruction: Instruction {
664 opcode: Opcode::Call,
665 dst: None,
666 src: None,
667 off: None,
668 imm: Some(Either::Left(target.to_string())),
669 span: 0..0,
670 },
671 offset,
672 }
673 }
674}