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