Skip to main content

sbpf_assembler/
ast.rs

1use {
2    crate::{
3        CompileError, SbpfArch,
4        astnode::{ASTNode, ROData},
5        dynsym::{DynamicSymbolMap, RelDynMap, RelocationType},
6        header::ProgramHeader,
7        optimizer,
8        parser::{ProgramLayout, Token},
9        section::{CodeSection, DataSection},
10    },
11    either::Either,
12    sbpf_common::{
13        inst_param::{Number, Register},
14        instruction::Instruction,
15        opcode::Opcode,
16    },
17    std::{
18        collections::{HashMap, HashSet},
19        path::PathBuf,
20    },
21    syscall_map::murmur3_32,
22};
23
24type LabelOffsetMap = HashMap<String, u64>;
25type NumericLabel = (String, u64, usize);
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct RodataRelocation {
29    // Offset of the relocation in rodata
30    pub offset: u64,
31    // Target label whose address should be written at the relocation offset
32    pub target: String,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum OptimizationConfig {
37    Disabled,
38    /// Runs all optimizations: dead-function elimination, then the ALU32
39    /// extension fix-up. The fix-up assumes the input follows eBPF semantics
40    /// (LLVM / sbpf-linker output), where every ALU32 op zero-extends, and
41    /// rewrites `add32`/`sub32`/`mul32` to keep that behaviour under SBPF.
42    Enabled {
43        cfg_dump_dir: Option<PathBuf>,
44    },
45}
46
47impl Default for OptimizationConfig {
48    fn default() -> Self {
49        Self::disabled()
50    }
51}
52
53impl OptimizationConfig {
54    pub fn disabled() -> Self {
55        Self::Disabled
56    }
57
58    pub fn enabled() -> Self {
59        Self::Enabled { cfg_dump_dir: None }
60    }
61
62    pub fn with_cfg_dump_dir(self, path: impl Into<PathBuf>) -> Self {
63        match self {
64            Self::Enabled { .. } => Self::Enabled {
65                cfg_dump_dir: Some(path.into()),
66            },
67            Self::Disabled => Self::Disabled,
68        }
69    }
70}
71
72#[derive(Default, Debug)]
73pub struct AST {
74    pub nodes: Vec<ASTNode>,
75    pub rodata_nodes: Vec<ASTNode>,
76
77    function_entries: HashSet<String>,
78    text_size: u64,
79    rodata_size: u64,
80    rodata_relocations: Vec<RodataRelocation>,
81}
82
83impl AST {
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    pub fn add_function_entry(&mut self, name: String) {
89        self.function_entries.insert(name);
90    }
91
92    pub fn add_rodata_relocation(&mut self, offset: u64, target: String) {
93        self.rodata_relocations
94            .push(RodataRelocation { offset, target });
95    }
96
97    pub(crate) fn function_entries(&self) -> &HashSet<String> {
98        &self.function_entries
99    }
100
101    pub(crate) fn rodata_relocations(&self) -> &[RodataRelocation] {
102        &self.rodata_relocations
103    }
104
105    //
106    pub fn set_text_size(&mut self, text_size: u64) {
107        self.text_size = text_size;
108    }
109
110    //
111    pub fn set_rodata_size(&mut self, rodata_size: u64) {
112        self.rodata_size = rodata_size;
113    }
114
115    //
116    pub fn get_instruction_at_offset(&mut self, offset: u64) -> Option<&mut Instruction> {
117        self.nodes
118            .iter_mut()
119            .find(|node| match node {
120                ASTNode::Instruction {
121                    instruction: _,
122                    offset: inst_offset,
123                    ..
124                } => offset == *inst_offset,
125                _ => false,
126            })
127            .map(|node| match node {
128                ASTNode::Instruction { instruction, .. } => instruction,
129                _ => panic!("Expected Instruction node"),
130            })
131    }
132
133    //
134    pub fn get_rodata_at_offset(&self, offset: u64) -> Option<&ROData> {
135        self.rodata_nodes
136            .iter()
137            .find(|node| match node {
138                ASTNode::ROData {
139                    rodata: _,
140                    offset: rodata_offset,
141                    ..
142                } => offset == *rodata_offset,
143                _ => false,
144            })
145            .map(|node| match node {
146                ASTNode::ROData { rodata, .. } => rodata,
147                _ => panic!("Expected ROData node"),
148            })
149    }
150
151    /// Resolve numeric label references (like "2f" or "1b")
152    pub(crate) fn resolve_numeric_label(
153        label_ref: &str,
154        current_idx: usize,
155        numeric_labels: &[NumericLabel],
156    ) -> Option<u64> {
157        if let Some(direction) = label_ref.chars().last()
158            && (direction == 'f' || direction == 'b')
159        {
160            let label_num = &label_ref[..label_ref.len() - 1];
161
162            if direction == 'f' {
163                // search forward from current position
164                for (name, offset, node_idx) in numeric_labels {
165                    if name == label_num && *node_idx > current_idx {
166                        return Some(*offset);
167                    }
168                }
169            } else {
170                // search backward from current position
171                for (name, offset, node_idx) in numeric_labels.iter().rev() {
172                    if name == label_num && *node_idx < current_idx {
173                        return Some(*offset);
174                    }
175                }
176            }
177        }
178        None
179    }
180}
181
182pub fn build_program(
183    mut ast: AST,
184    arch: SbpfArch,
185    optimization: OptimizationConfig,
186) -> Result<ProgramLayout, Vec<CompileError>> {
187    let optimization = run_optimizations(&mut ast, &optimization);
188    let mut errors = optimization.errors;
189
190    let (label_offset_map, numeric_labels) = label_offset_map(&ast);
191    let program_is_static = arch.is_v3()
192        || (ast.rodata_relocations.is_empty()
193            && !ast.nodes.iter().any(|node| {
194                matches!(node, ASTNode::Instruction { instruction: inst, .. }
195                    if inst.is_syscall()
196                    || (inst.opcode == Opcode::Lddw && matches!(&inst.imm, Some(Either::Left(_)))))
197            }));
198
199    let label_resolution = resolve_label_references(
200        &mut ast,
201        arch,
202        program_is_static,
203        &label_offset_map,
204        &numeric_labels,
205    );
206    errors.extend(label_resolution.errors);
207
208    optimizer::remove_temp_control_flow_target_labels(
209        &mut ast.nodes,
210        &optimization.labels_to_remove,
211    );
212
213    if !errors.is_empty() {
214        Err(errors)
215    } else {
216        Ok(ProgramLayout {
217            code_section: CodeSection::new(std::mem::take(&mut ast.nodes), ast.text_size)
218                .with_arch(arch),
219            data_section: DataSection::new(std::mem::take(&mut ast.rodata_nodes), ast.rodata_size),
220            dynamic_symbols: label_resolution.dynamic_symbols,
221            relocation_data: label_resolution.relocations,
222            prog_is_static: program_is_static,
223            arch,
224            debug_sections: Vec::default(),
225        })
226    }
227}
228
229#[derive(Default)]
230struct OptimizationOutcome {
231    labels_to_remove: HashSet<String>,
232    errors: Vec<CompileError>,
233}
234
235fn run_optimizations(ast: &mut AST, config: &OptimizationConfig) -> OptimizationOutcome {
236    let OptimizationConfig::Enabled { cfg_dump_dir } = config else {
237        return OptimizationOutcome::default();
238    };
239
240    // Normalize numeric and relative control-flow targets to labels before running
241    // CFG-based optimizations. Synthetic labels are tracked so they can be removed
242    // after optimization, and the pass is skipped if normalization fails.
243    let canonicalized_targets = optimizer::canonicalize_control_flow_targets(&mut ast.nodes);
244    let labels_to_remove = canonicalized_targets.labels_to_remove;
245    let mut errors = Vec::new();
246
247    if canonicalized_targets.errors.is_empty() {
248        if let Some(dump_dir) = cfg_dump_dir.as_deref() {
249            let mut dump_errors = Vec::new();
250            if let Err(error) = std::fs::create_dir_all(dump_dir) {
251                dump_errors.push((dump_dir.to_path_buf(), error));
252                optimizer::optimize(ast);
253            } else {
254                optimizer::optimize_with_observer(ast, |stage, cfg| {
255                    let path = dump_dir.join(stage.file_name());
256                    if let Err(error) = std::fs::write(&path, sbpf_analyze::dump_cfg(cfg)) {
257                        dump_errors.push((path, error));
258                    }
259                });
260            }
261            for (path, error) in dump_errors {
262                errors.push(CompileError::BytecodeError {
263                    error: format!("failed to write CFG dump '{}': {error}", path.display()),
264                    span: 0..0,
265                    custom_label: None,
266                });
267            }
268        } else {
269            optimizer::optimize(ast);
270        }
271    }
272
273    OptimizationOutcome {
274        labels_to_remove,
275        errors,
276    }
277}
278
279#[derive(Default)]
280struct LabelResolution {
281    dynamic_symbols: DynamicSymbolMap,
282    relocations: RelDynMap,
283    errors: Vec<CompileError>,
284}
285
286fn resolve_label_references(
287    ast: &mut AST,
288    arch: SbpfArch,
289    program_is_static: bool,
290    label_offset_map: &LabelOffsetMap,
291    numeric_labels: &[NumericLabel],
292) -> LabelResolution {
293    let mut relocations = RelDynMap::new();
294    let mut dynamic_symbols = DynamicSymbolMap::new();
295    let mut errors = Vec::new();
296
297    // Resolve both static and dynamic syscalls.
298    for node in ast.nodes.iter_mut() {
299        if let ASTNode::Instruction {
300            instruction: inst,
301            offset,
302        } = node
303            && inst.is_syscall()
304            && let Some(Either::Left(syscall_name)) = &inst.imm
305        {
306            let syscall_name = syscall_name.clone();
307            if arch.is_v3() {
308                // Static syscall: src = 0, imm = hash
309                inst.src = Some(Register { n: 0 });
310                inst.imm = Some(Either::Right(Number::Int(murmur3_32(&syscall_name) as i64)));
311            } else {
312                // Dynamic syscall: src = 1, imm = -1
313                inst.src = Some(Register { n: 1 });
314                inst.imm = Some(Either::Right(Number::Int(-1)));
315
316                // Add relocation for dynamic syscall
317                relocations.add_rel_dyn(*offset, RelocationType::RSbfSyscall, syscall_name.clone());
318                dynamic_symbols.add_call_target(syscall_name.clone(), *offset);
319            }
320        }
321    }
322
323    for (idx, node) in ast.nodes.iter_mut().enumerate() {
324        if let ASTNode::Instruction {
325            instruction: inst,
326            offset,
327            ..
328        } = node
329        {
330            // For jump/call instructions, replace label with relative offsets
331            if inst.is_jump()
332                && let Some(Either::Left(label)) = &inst.off
333            {
334                let target_offset = if let Some(offset) = label_offset_map.get(label) {
335                    Some(*offset)
336                } else {
337                    // Handle numeric label references
338                    AST::resolve_numeric_label(label, idx, numeric_labels)
339                };
340
341                if let Some(target_offset) = target_offset {
342                    let rel_offset = (target_offset as i64 - *offset as i64) / 8 - 1;
343                    inst.off = Some(Either::Right(rel_offset as i16));
344                }
345            } else if inst.opcode == Opcode::Call
346                && let Some(Either::Left(label)) = &inst.imm
347                && let Some(target_offset) = label_offset_map.get(label)
348            {
349                let rel_offset = (*target_offset as i64 - *offset as i64) / 8 - 1;
350                inst.src = Some(Register { n: 1 });
351                inst.imm = Some(Either::Right(Number::Int(rel_offset)));
352            }
353
354            if inst.opcode == Opcode::Lddw
355                && let Some(Either::Left(name)) = &inst.imm
356            {
357                let label = name.clone();
358                // Add relocation for lddw (only for v0)
359                if !arch.is_v3() {
360                    relocations.add_rel_dyn(*offset, RelocationType::RSbf64Relative, label.clone());
361                }
362
363                if let Some(target_offset) = label_offset_map.get(&label) {
364                    let abs_offset = if arch.is_v3() {
365                        if *target_offset >= ast.text_size {
366                            (ProgramHeader::V3_RODATA_VADDR + *target_offset - ast.text_size) as i64
367                        } else {
368                            (ProgramHeader::V3_BYTECODE_VADDR + *target_offset) as i64
369                        }
370                    } else {
371                        let ph_count = if program_is_static { 1 } else { 3 };
372                        let ph_offset = 64 + (ph_count as u64 * 56) as i64;
373                        *target_offset as i64 + ph_offset
374                    };
375                    // Replace label with immediate value
376                    inst.imm = Some(Either::Right(Number::Addr(abs_offset)));
377                }
378            }
379
380            // Check if any identifier in `inst.off` or `inst.imm` is unresolved.
381            if let Some(Either::Left(label)) = &inst.off {
382                errors.push(CompileError::UndefinedLabel {
383                    label: label.clone(),
384                    span: inst.span.clone(),
385                    custom_label: None,
386                });
387            }
388            if let Some(Either::Left(label)) = &inst.imm {
389                errors.push(CompileError::UndefinedLabel {
390                    label: label.clone(),
391                    span: inst.span.clone(),
392                    custom_label: None,
393                });
394            }
395        }
396    }
397
398    // Set entry point offset if a GlobalDecl was specified
399    let entry_label = ast.nodes.iter().find_map(|node| {
400        if let ASTNode::GlobalDecl { global_decl } = node {
401            Some(global_decl.entry_label.clone())
402        } else {
403            None
404        }
405    });
406    if let Some(entry_label) = entry_label
407        && let Some(offset) = label_offset_map.get(&entry_label)
408    {
409        dynamic_symbols.add_entry_point(entry_label, *offset);
410    }
411
412    // Resolve rodata relocations and write their target addresses.
413    for relocation in &ast.rodata_relocations {
414        let Some(&target_offset) = label_offset_map.get(&relocation.target) else {
415            errors.push(CompileError::UndefinedLabel {
416                label: relocation.target.clone(),
417                span: 0..0,
418                custom_label: None,
419            });
420            continue;
421        };
422        let value = if arch.is_v3() {
423            if target_offset >= ast.text_size {
424                ProgramHeader::V3_RODATA_VADDR + target_offset - ast.text_size
425            } else {
426                ProgramHeader::V3_BYTECODE_VADDR + target_offset
427            }
428        } else {
429            let ph_count = if program_is_static { 1 } else { 3 };
430            (64 + ph_count * 56 + target_offset) << 32
431        };
432
433        let mut written = false;
434        for node in &mut ast.rodata_nodes {
435            let ASTNode::ROData { rodata, offset } = node else {
436                continue;
437            };
438            let is_quad = match rodata.args.first() {
439                Some(Token::Directive(directive, _)) => directive == "quad",
440                _ => continue,
441            };
442            let Some(Token::VectorLiteral(bytes, _)) = rodata.args.get_mut(1) else {
443                continue;
444            };
445            let Some(relocation_offset_in_node) = relocation.offset.checked_sub(*offset) else {
446                continue;
447            };
448
449            // Write the target address at the relocation offset.
450            if is_quad {
451                // Replace one 8-byte element
452                if relocation_offset_in_node % 8 == 0
453                    && let Some(relocation_value) =
454                        bytes.get_mut((relocation_offset_in_node / 8) as usize)
455                {
456                    *relocation_value = Number::Int(value as i64);
457                    written = true;
458                    break;
459                }
460            } else {
461                // Replace individual byte elements
462                if let Some(relocation_bytes) = bytes.get_mut(
463                    relocation_offset_in_node as usize..relocation_offset_in_node as usize + 8,
464                ) {
465                    for (byte, value) in relocation_bytes.iter_mut().zip(value.to_le_bytes()) {
466                        *byte = Number::Int(i64::from(value));
467                    }
468                    written = true;
469                    break;
470                }
471            }
472        }
473
474        if !written {
475            errors.push(CompileError::BytecodeError {
476                error: format!(
477                    "could not apply rodata relocation at offset {}",
478                    relocation.offset
479                ),
480                span: 0..0,
481                custom_label: None,
482            });
483            continue;
484        }
485
486        if !arch.is_v3() {
487            relocations.add_rel_dyn(
488                ast.text_size + relocation.offset,
489                RelocationType::RSbf64Relative,
490                String::new(),
491            );
492        }
493    }
494
495    LabelResolution {
496        dynamic_symbols,
497        relocations,
498        errors,
499    }
500}
501
502fn label_offset_map(ast: &AST) -> (LabelOffsetMap, Vec<NumericLabel>) {
503    let mut label_offset_map = HashMap::new();
504    let mut numeric_labels = Vec::new();
505
506    for (idx, node) in ast.nodes.iter().enumerate() {
507        if let ASTNode::Label { label, offset } = node {
508            label_offset_map.insert(label.name.clone(), *offset);
509            numeric_labels.push((label.name.clone(), *offset, idx));
510        }
511    }
512
513    for node in &ast.rodata_nodes {
514        if let ASTNode::ROData { rodata, offset } = node {
515            label_offset_map.insert(rodata.name.clone(), *offset + ast.text_size);
516        }
517    }
518
519    (label_offset_map, numeric_labels)
520}
521
522#[cfg(test)]
523mod tests {
524    use {
525        super::*,
526        crate::{astnode::Label, parser::Token, section::Section},
527    };
528
529    #[test]
530    fn test_ast_new() {
531        let ast = AST::new();
532        assert!(ast.nodes.is_empty());
533        assert!(ast.rodata_nodes.is_empty());
534        assert_eq!(ast.text_size, 0);
535        assert_eq!(ast.rodata_size, 0);
536    }
537
538    #[test]
539    fn test_ast_set_sizes() {
540        let mut ast = AST::new();
541        ast.set_text_size(100);
542        ast.set_rodata_size(50);
543        assert_eq!(ast.text_size, 100);
544        assert_eq!(ast.rodata_size, 50);
545    }
546
547    #[test]
548    fn test_get_instruction_at_offset() {
549        let mut ast = AST::new();
550        ast.nodes
551            .push(instruction_node(Opcode::Exit, 0, None, None));
552
553        let found = ast.get_instruction_at_offset(0);
554        assert!(found.is_some());
555        assert_eq!(found.unwrap().opcode, Opcode::Exit);
556
557        let not_found = ast.get_instruction_at_offset(8);
558        assert!(not_found.is_none());
559    }
560
561    #[test]
562    fn test_get_rodata_at_offset() {
563        let mut ast = AST::new();
564        let rodata = ROData {
565            name: "data".to_string(),
566            args: vec![
567                Token::Directive("ascii".to_string(), 0..5),
568                Token::StringLiteral("test".to_string(), 6..12),
569            ],
570            span: 0..12,
571        };
572        ast.rodata_nodes.push(ASTNode::ROData {
573            rodata: rodata.clone(),
574            offset: 0,
575        });
576
577        let found = ast.get_rodata_at_offset(0);
578        assert!(found.is_some());
579        assert_eq!(found.unwrap().name, "data");
580    }
581
582    #[test]
583    fn test_resolve_numeric_label_forward() {
584        let numeric_labels = vec![("1".to_string(), 16, 2), ("2".to_string(), 32, 4)];
585
586        let result = AST::resolve_numeric_label("1f", 0, &numeric_labels);
587        assert_eq!(result, Some(16));
588
589        let result = AST::resolve_numeric_label("2f", 3, &numeric_labels);
590        assert_eq!(result, Some(32));
591    }
592
593    #[test]
594    fn test_resolve_numeric_label_backward() {
595        let numeric_labels = vec![("1".to_string(), 16, 2), ("2".to_string(), 32, 4)];
596
597        let result = AST::resolve_numeric_label("1b", 3, &numeric_labels);
598        assert_eq!(result, Some(16));
599
600        let result = AST::resolve_numeric_label("2b", 5, &numeric_labels);
601        assert_eq!(result, Some(32));
602    }
603
604    #[test]
605    fn test_canonicalize_numeric_jump_target_to_label() {
606        let mut nodes = vec![
607            instruction_node(Opcode::Ja, 0, Some(Either::Left("1f".to_string())), None),
608            label_node("1", 8),
609            instruction_node(Opcode::Exit, 8, None, None),
610        ];
611
612        let canonicalized = optimizer::canonicalize_control_flow_targets(&mut nodes);
613
614        assert!(canonicalized.errors.is_empty());
615        assert!(canonicalized.labels_to_remove.is_empty());
616        let ASTNode::Instruction { instruction, .. } = &nodes[0] else {
617            panic!("expected instruction");
618        };
619        assert_eq!(instruction.off, Some(Either::Left("1".to_string())));
620    }
621
622    #[test]
623    fn test_canonicalize_relative_jump_target_to_synthetic_label() {
624        let mut nodes = vec![
625            instruction_node(Opcode::Ja, 0, Some(Either::Right(1)), None),
626            instruction_node(Opcode::Exit, 8, None, None),
627            instruction_node(Opcode::Exit, 16, None, None),
628        ];
629
630        let canonicalized = optimizer::canonicalize_control_flow_targets(&mut nodes);
631
632        assert!(canonicalized.errors.is_empty());
633        assert_eq!(canonicalized.labels_to_remove.len(), 1);
634
635        let ASTNode::Instruction { instruction, .. } = &nodes[0] else {
636            panic!("expected instruction");
637        };
638        let Some(Either::Left(label)) = &instruction.off else {
639            panic!("expected canonical label target");
640        };
641        assert!(label.starts_with("temp_"));
642        assert!(canonicalized.labels_to_remove.contains(label));
643        assert!(
644            matches!(&nodes[2], ASTNode::Label { label: target_label, offset }
645                if target_label.name == *label && *offset == 16)
646        );
647    }
648
649    #[test]
650    fn test_canonicalize_rejects_invalid_relative_jump_target() {
651        let mut nodes = vec![
652            instruction_node(Opcode::Ja, 0, Some(Either::Right(1)), None),
653            instruction_node(Opcode::Exit, 8, None, None),
654        ];
655
656        let canonicalized = optimizer::canonicalize_control_flow_targets(&mut nodes);
657
658        assert_eq!(canonicalized.errors.len(), 1);
659        assert!(canonicalized.labels_to_remove.is_empty());
660        assert_eq!(nodes.len(), 2);
661        let ASTNode::Instruction { instruction, .. } = &nodes[0] else {
662            panic!("expected instruction");
663        };
664        assert_eq!(instruction.off, Some(Either::Right(1)));
665    }
666
667    #[test]
668    fn test_dce_recomputes_relative_call_target_after_removing_code() {
669        let mut ast = AST::new();
670        ast.add_function_entry("entrypoint".to_string());
671        ast.add_function_entry("dead".to_string());
672        ast.add_function_entry("target".to_string());
673        ast.nodes = vec![
674            label_node("entrypoint", 0),
675            internal_call_node(0, 2),
676            instruction_node(Opcode::Exit, 8, None, None),
677            label_node("dead", 16),
678            instruction_node(Opcode::Exit, 16, None, None),
679            label_node("target", 24),
680            instruction_node(Opcode::Exit, 24, None, None),
681        ];
682        ast.set_text_size(32);
683
684        let program_layout =
685            build_program(ast, SbpfArch::V0, OptimizationConfig::enabled()).unwrap();
686        let nodes = program_layout.code_section.get_nodes();
687
688        assert_eq!(
689            nodes
690                .iter()
691                .filter(|node| matches!(node, ASTNode::Instruction { .. }))
692                .count(),
693            3
694        );
695        assert!(matches!(
696            &nodes[1],
697            ASTNode::Instruction { instruction, offset }
698                if instruction.opcode == Opcode::Call
699                    && instruction.src == Some(Register { n: 1 })
700                    && instruction.imm == Some(Either::Right(Number::Int(1)))
701                    && *offset == 0
702        ));
703        assert!(matches!(
704            &nodes[3],
705            ASTNode::Label { label, offset } if label.name == "target" && *offset == 16
706        ));
707    }
708
709    #[test]
710    fn test_optimize_ast_removes_temp_jump_target_labels() {
711        let mut ast = AST::new();
712        ast.nodes = vec![
713            instruction_node(Opcode::Ja, 0, Some(Either::Right(1)), None),
714            instruction_node(Opcode::Exit, 8, None, None),
715            instruction_node(Opcode::Exit, 16, None, None),
716        ];
717        ast.set_text_size(24);
718        ast.set_rodata_size(0);
719
720        let result = build_program(ast, SbpfArch::V0, OptimizationConfig::enabled());
721
722        assert!(result.is_ok());
723        let program_layout = result.unwrap();
724        assert!(!program_layout.code_section.get_nodes().iter().any(|node| {
725            matches!(node, ASTNode::Label { label, .. }
726                if label
727                    .name
728                    .starts_with("temp_"))
729        }));
730        let ASTNode::Instruction { instruction, .. } = &program_layout.code_section.get_nodes()[0]
731        else {
732            panic!("expected instruction");
733        };
734        assert_eq!(instruction.off, Some(Either::Right(1)));
735    }
736
737    #[test]
738    fn test_build_program_simple() {
739        for arch in [SbpfArch::V0, SbpfArch::V3] {
740            let mut ast = AST::new();
741            ast.nodes
742                .push(instruction_node(Opcode::Exit, 0, None, None));
743            ast.set_text_size(8);
744            ast.set_rodata_size(0);
745
746            let result = build_program(ast, arch, OptimizationConfig::default());
747            assert!(result.is_ok());
748            let parse_result = result.unwrap();
749            assert!(parse_result.prog_is_static);
750        }
751    }
752
753    #[test]
754    fn test_build_program_undefined_label_error() {
755        for arch in [SbpfArch::V0, SbpfArch::V3] {
756            let mut ast = AST::new();
757            ast.nodes.push(instruction_node(
758                Opcode::Ja,
759                0,
760                Some(Either::Left("undefined_label".to_string())),
761                None,
762            ));
763            ast.set_text_size(8);
764
765            let result = build_program(ast, arch, OptimizationConfig::default());
766            assert!(result.is_err());
767        }
768    }
769
770    #[test]
771    fn test_build_program_static_syscalls_no_relocation() {
772        let mut ast = AST::new();
773
774        ast.nodes.push(instruction_node(
775            Opcode::Call,
776            0,
777            None,
778            Some(Either::Left("sol_log_".to_string())),
779        ));
780        ast.nodes
781            .push(instruction_node(Opcode::Exit, 8, None, None));
782
783        ast.set_text_size(16);
784        ast.set_rodata_size(0);
785
786        let result = build_program(ast, SbpfArch::V3, OptimizationConfig::default());
787        assert!(result.is_ok());
788        let parse_result = result.unwrap();
789
790        assert!(parse_result.prog_is_static);
791        assert!(parse_result.relocation_data.get_rel_dyns().is_empty());
792    }
793
794    #[test]
795    fn test_build_program_dynamic_syscalls_with_relocation() {
796        let mut ast = AST::new();
797
798        ast.nodes.push(instruction_node(
799            Opcode::Call,
800            0,
801            None,
802            Some(Either::Left("sol_log_".to_string())),
803        ));
804        ast.nodes
805            .push(instruction_node(Opcode::Exit, 8, None, None));
806
807        ast.set_text_size(16);
808        ast.set_rodata_size(0);
809
810        let result = build_program(ast, SbpfArch::V0, OptimizationConfig::default());
811        assert!(result.is_ok());
812        let parse_result = result.unwrap();
813
814        assert!(!parse_result.prog_is_static);
815        assert!(!parse_result.relocation_data.get_rel_dyns().is_empty());
816    }
817
818    #[test]
819    fn test_build_program_rodata_data_relocation_v0() {
820        let result = build_program(
821            rodata_data_relocation_ast(),
822            SbpfArch::V0,
823            OptimizationConfig::default(),
824        );
825        assert!(result.is_ok());
826        let layout = result.unwrap();
827        assert!(!layout.prog_is_static);
828        let target_address: u64 = 64 + 3 * 56 + 8 + 8;
829        assert_eq!(
830            &layout.data_section.bytecode()[..8],
831            &(target_address << 32).to_le_bytes()
832        );
833    }
834
835    #[test]
836    fn test_build_program_rodata_data_relocation_v3() {
837        let result = build_program(
838            rodata_data_relocation_ast(),
839            SbpfArch::V3,
840            OptimizationConfig::default(),
841        );
842        assert!(result.is_ok());
843        let layout = result.unwrap();
844        assert!(layout.prog_is_static);
845        assert_eq!(
846            &layout.data_section.bytecode()[..8],
847            &(ProgramHeader::V3_RODATA_VADDR + 8).to_le_bytes()
848        );
849    }
850
851    #[test]
852    fn test_build_program_rodata_fn_relocation_v0() {
853        let result = build_program(
854            rodata_fn_relocation_ast(),
855            SbpfArch::V0,
856            OptimizationConfig::default(),
857        );
858        assert!(result.is_ok());
859        let layout = result.unwrap();
860        assert!(!layout.prog_is_static);
861        let target_address: u64 = 64 + 3 * 56;
862        assert_eq!(
863            &layout.data_section.bytecode()[..8],
864            &(target_address << 32).to_le_bytes()
865        );
866    }
867
868    #[test]
869    fn test_build_program_rodata_fn_relocation_v3() {
870        let result = build_program(
871            rodata_fn_relocation_ast(),
872            SbpfArch::V3,
873            OptimizationConfig::default(),
874        );
875        assert!(result.is_ok());
876        let layout = result.unwrap();
877        assert!(layout.prog_is_static);
878        assert_eq!(
879            &layout.data_section.bytecode()[..8],
880            &ProgramHeader::V3_BYTECODE_VADDR.to_le_bytes()
881        );
882    }
883
884    #[test]
885    fn test_build_program_rodata_relocation_undefined_label() {
886        let mut ast = AST::new();
887        ast.nodes
888            .push(instruction_node(Opcode::Exit, 0, None, None));
889        ast.add_rodata_relocation(8, "invalid".to_string());
890        ast.set_text_size(8);
891        let result = build_program(ast, SbpfArch::V0, OptimizationConfig::default());
892        let Err(errors) = result else {
893            panic!("expected undefined label");
894        };
895        assert!(matches!(
896            &errors[..],
897            [CompileError::UndefinedLabel { label, .. }] if label == "invalid"
898        ));
899    }
900
901    fn label_node(name: &str, offset: u64) -> ASTNode {
902        ASTNode::Label {
903            label: Label {
904                name: name.to_string(),
905                span: 0..0,
906            },
907            offset,
908        }
909    }
910
911    fn instruction_node(
912        opcode: Opcode,
913        offset: u64,
914        off: Option<Either<String, i16>>,
915        imm: Option<Either<String, Number>>,
916    ) -> ASTNode {
917        ASTNode::Instruction {
918            instruction: Instruction {
919                opcode,
920                dst: None,
921                src: None,
922                off,
923                imm,
924                span: 0..0,
925            },
926            offset,
927        }
928    }
929
930    fn internal_call_node(offset: u64, relative_offset: i64) -> ASTNode {
931        ASTNode::Instruction {
932            instruction: Instruction {
933                opcode: Opcode::Call,
934                dst: None,
935                src: Some(Register { n: 1 }),
936                off: None,
937                imm: Some(Either::Right(Number::Int(relative_offset))),
938                span: 0..0,
939            },
940            offset,
941        }
942    }
943
944    // Test AST with a rodata relocation targeting data in the rodata section.
945    fn rodata_data_relocation_ast() -> AST {
946        let mut ast = AST::new();
947        ast.nodes
948            .push(instruction_node(Opcode::Exit, 0, None, None));
949        ast.rodata_nodes.push(ASTNode::ROData {
950            rodata: ROData {
951                name: "ptr".to_string(),
952                args: vec![
953                    Token::Directive("byte".to_string(), 0..1),
954                    Token::VectorLiteral(vec![Number::Int(0); 8], 0..1),
955                ],
956                span: 0..1,
957            },
958            offset: 0,
959        });
960        ast.rodata_nodes.push(ASTNode::ROData {
961            rodata: ROData {
962                name: "data".to_string(),
963                args: vec![
964                    Token::Directive("byte".to_string(), 0..1),
965                    Token::VectorLiteral(vec![Number::Int(0); 8], 0..1),
966                ],
967                span: 0..1,
968            },
969            offset: 8,
970        });
971        ast.add_rodata_relocation(0, "data".to_string());
972        ast.set_text_size(8);
973        ast.set_rodata_size(16);
974        ast
975    }
976
977    // Test AST with a rodata relocation targeting a function in the text section.
978    fn rodata_fn_relocation_ast() -> AST {
979        let mut ast = AST::new();
980        ast.nodes.push(label_node("fn", 0));
981        ast.nodes
982            .push(instruction_node(Opcode::Exit, 0, None, None));
983        ast.rodata_nodes.push(ASTNode::ROData {
984            rodata: ROData {
985                name: "ptr".to_string(),
986                args: vec![
987                    Token::Directive("byte".to_string(), 0..1),
988                    Token::VectorLiteral(vec![Number::Int(0); 8], 0..1),
989                ],
990                span: 0..1,
991            },
992            offset: 0,
993        });
994        ast.add_rodata_relocation(0, "fn".to_string());
995        ast.set_text_size(8);
996        ast.set_rodata_size(8);
997        ast
998    }
999}