Skip to main content

sbpf_assembler/
lib.rs

1use {anyhow::Result, codespan::Files};
2
3// Parser
4pub mod parser;
5
6// Preprocessor (include + macro expansion)
7pub mod preprocessor;
8
9// Error handling and diagnostics
10pub mod errors;
11pub mod macros;
12
13// Intermediate Representation
14pub mod ast;
15pub mod astnode;
16pub mod dynsym;
17
18// ELF header, program, section
19pub mod header;
20pub mod program;
21pub mod section;
22
23// Debug info
24pub mod debug;
25
26// WASM bindings
27#[cfg(target_arch = "wasm32")]
28pub mod wasm;
29
30pub use self::{
31    astnode::ASTNode,
32    debug::DebugData,
33    errors::CompileError,
34    parser::{ParseResult, Token, parse},
35    preprocessor::{
36        FileResolver, FsFileResolver, MockFileResolver, PreprocessResult, preprocess,
37        source_map::{FileRegistry, SourceMap, SourceOrigin},
38    },
39    program::Program,
40};
41
42/// sBPF target architecture
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub enum SbpfArch {
45    #[default]
46    V0,
47    V3,
48}
49
50impl SbpfArch {
51    pub fn is_v3(&self) -> bool {
52        matches!(self, SbpfArch::V3)
53    }
54
55    pub fn e_flags(&self) -> u32 {
56        match self {
57            SbpfArch::V0 => 0,
58            SbpfArch::V3 => 3,
59        }
60    }
61}
62
63/// Debug mode configuration for the assembler
64#[derive(Debug, Clone)]
65pub struct DebugMode {
66    /// Source filename for debug info
67    pub filename: String,
68    /// Source directory for debug info
69    pub directory: String,
70}
71
72/// Options for the assembler
73#[derive(Debug, Clone, Default)]
74pub struct AssemblerOption {
75    /// sBPF target architecture
76    pub arch: SbpfArch,
77    /// Optional debug mode configuration
78    pub debug_mode: Option<DebugMode>,
79}
80
81/// An error enriched with source location information from preprocessing.
82/// Wraps a `CompileError` with the resolved original source location.
83#[derive(Debug)]
84pub struct AssemblerError {
85    pub error: CompileError,
86    pub origin: Option<SourceOrigin>,
87    /// Column offset (0-based) within the original line, if known.
88    pub column: Option<usize>,
89}
90
91impl std::fmt::Display for AssemblerError {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(f, "{}", self.error)
94    }
95}
96
97impl std::error::Error for AssemblerError {
98    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
99        Some(&self.error)
100    }
101}
102
103/// Returned when assembly with preprocessing fails.
104/// Contains the errors and the file registry so callers can render
105/// diagnostics against the original source files.
106#[derive(Debug)]
107pub struct AssembleErrors {
108    pub errors: Vec<AssemblerError>,
109    pub file_registry: FileRegistry,
110}
111
112/// Assembler for SBPF assembly code
113#[derive(Debug, Clone)]
114pub struct Assembler {
115    options: AssemblerOption,
116}
117
118impl Assembler {
119    /// Create a new Assembler with the given options
120    pub fn new(options: AssemblerOption) -> Self {
121        Self { options }
122    }
123
124    /// Assemble source code directly (no preprocessing).
125    /// This is the original API -- macros and includes are not supported.
126    pub fn assemble(&self, source: &str) -> Result<Vec<u8>, Vec<CompileError>> {
127        let parse_result = match parse(source, self.options.arch) {
128            Ok(result) => result,
129            Err(errors) => {
130                return Err(errors);
131            }
132        };
133
134        // Build debug data if debug mode is enabled
135        let debug_data = if let Some(ref debug_mode) = self.options.debug_mode {
136            let (lines, labels) = collect_line_and_label_entries(source, &parse_result);
137            let code_end = parse_result.code_section.get_size();
138
139            Some(DebugData {
140                filename: debug_mode.filename.clone(),
141                directory: debug_mode.directory.clone(),
142                lines,
143                labels,
144                code_start: 0,
145                code_end,
146            })
147        } else {
148            None
149        };
150
151        let program = Program::from_parse_result(parse_result, debug_data);
152        let bytecode = program.emit_bytecode();
153        Ok(bytecode)
154    }
155
156    /// Assemble with preprocessing: resolves `.include` and expands `.macro` directives
157    /// before parsing. Errors include source location information from the source map,
158    /// and the file registry is returned so callers can render diagnostics against
159    /// original source files.
160    pub fn assemble_with_preprocess(
161        &self,
162        source: &str,
163        source_path: &str,
164        resolver: Option<&dyn FileResolver>,
165    ) -> Result<Vec<u8>, AssembleErrors> {
166        // Run preprocessor
167        let preprocess_result =
168            preprocess(source, source_path, resolver).map_err(|failure| AssembleErrors {
169                errors: failure
170                    .errors
171                    .into_iter()
172                    .map(|e| AssemblerError {
173                        error: e.error,
174                        origin: e.origin,
175                        column: None,
176                    })
177                    .collect(),
178                file_registry: failure.file_registry,
179            })?;
180
181        let expanded = &preprocess_result.expanded_source;
182        let source_map = &preprocess_result.source_map;
183
184        // Parse the expanded source
185        let parse_result = match parse(expanded, self.options.arch) {
186            Ok(result) => result,
187            Err(errors) => {
188                // Extract file registry from source map before moving errors
189                let file_registry = source_map.file_registry.clone();
190                return Err(AssembleErrors {
191                    errors: errors
192                        .into_iter()
193                        .map(|e| {
194                            let span = e.span();
195                            let origin = source_map.resolve_span(span, expanded).clone();
196                            // Compute column offset within the line from the
197                            // expanded source so we can highlight the right token.
198                            let col = expanded[..span.start]
199                                .rfind('\n')
200                                .map(|nl| span.start - nl - 1)
201                                .unwrap_or(span.start);
202                            AssemblerError {
203                                error: e,
204                                column: Some(col),
205                                origin: Some(origin),
206                            }
207                        })
208                        .collect(),
209                    file_registry,
210                });
211            }
212        };
213
214        // Build debug data if debug mode is enabled
215        let debug_data = if let Some(ref debug_mode) = self.options.debug_mode {
216            let (lines, labels) = collect_line_and_label_entries(expanded, &parse_result);
217            let code_end = parse_result.code_section.get_size();
218
219            Some(DebugData {
220                filename: debug_mode.filename.clone(),
221                directory: debug_mode.directory.clone(),
222                lines,
223                labels,
224                code_start: 0,
225                code_end,
226            })
227        } else {
228            None
229        };
230
231        let program = Program::from_parse_result(parse_result, debug_data);
232        let bytecode = program.emit_bytecode();
233        Ok(bytecode)
234    }
235
236    /// Convenience method: read a file from disk and assemble with full preprocessing.
237    pub fn assemble_file(&self, path: &std::path::Path) -> Result<Vec<u8>, AssembleErrors> {
238        let source = std::fs::read_to_string(path).map_err(|e| AssembleErrors {
239            errors: vec![AssemblerError {
240                error: CompileError::IncludeReadError {
241                    path: path.display().to_string(),
242                    reason: e.to_string(),
243                    span: 0..0,
244                    custom_label: Some("Failed to read source file".to_string()),
245                },
246                origin: None,
247                column: None,
248            }],
249            file_registry: FileRegistry::new(),
250        })?;
251
252        let source_path = path.to_string_lossy();
253        let resolver = FsFileResolver::new();
254        self.assemble_with_preprocess(&source, &source_path, Some(&resolver))
255    }
256}
257
258type LineEntry = (u64, u32); // (offset, line)
259type LabelEntry = (String, u64, u32); // (label, offset, line)
260
261/// Helper function to collect line and label entries
262fn collect_line_and_label_entries(
263    source: &str,
264    parse_result: &ParseResult,
265) -> (Vec<LineEntry>, Vec<LabelEntry>) {
266    let mut files: Files<&str> = Files::new();
267    let file_id = files.add("source", source);
268
269    let mut line_entries = Vec::new();
270    let mut label_entries = Vec::new();
271
272    for node in parse_result.code_section.get_nodes() {
273        match node {
274            ASTNode::Instruction {
275                instruction,
276                offset,
277            } => {
278                let line_index = files.line_index(file_id, instruction.span.start as u32);
279                let line_number = (line_index.to_usize() + 1) as u32;
280                line_entries.push((*offset, line_number));
281            }
282            ASTNode::Label { label, offset } => {
283                let line_index = files.line_index(file_id, label.span.start as u32);
284                let line_number = (line_index.to_usize() + 1) as u32;
285                label_entries.push((label.name.clone(), *offset, line_number));
286            }
287            _ => {}
288        }
289    }
290
291    for node in parse_result.data_section.get_nodes() {
292        if let ASTNode::ROData { rodata, offset } = node {
293            let line_index = files.line_index(file_id, rodata.span.start as u32);
294            let line_number = (line_index.to_usize() + 1) as u32;
295            label_entries.push((rodata.name.clone(), *offset, line_number));
296        }
297    }
298
299    (line_entries, label_entries)
300}
301
302#[cfg(test)]
303pub fn assemble(source: &str) -> Result<Vec<u8>, Vec<CompileError>> {
304    let options = AssemblerOption::default();
305    let assembler = Assembler::new(options);
306    assembler.assemble(source)
307}
308
309#[cfg(test)]
310pub fn assemble_with_debug_data(
311    source: &str,
312    filename: &str,
313    directory: &str,
314) -> Result<Vec<u8>, Vec<CompileError>> {
315    let options = AssemblerOption {
316        arch: SbpfArch::V0,
317        debug_mode: Some(DebugMode {
318            filename: filename.to_string(),
319            directory: directory.to_string(),
320        }),
321    };
322    let assembler = Assembler::new(options);
323    assembler.assemble(source)
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_assemble_success() {
332        let source = "exit";
333        let result = assemble(source);
334        assert!(result.is_ok());
335        let bytecode = result.unwrap();
336        assert!(!bytecode.is_empty());
337    }
338
339    #[test]
340    fn test_assemble_parse_error() {
341        let source = "invalid_xyz";
342        let result = assemble(source);
343        assert!(result.is_err());
344    }
345
346    #[test]
347    fn test_parse_error_has_correct_span() {
348        // Verify parse errors point to the actual error position,
349        // not span 0..len (which would lose position info for source maps).
350        let source = ".rodata\n    thing2: .bogus 5\n.text\nentrypoint:\n    exit\n";
351        let result = assemble(source);
352        assert!(result.is_err());
353        let errors = result.unwrap_err();
354        let span = errors[0].span();
355        // The error should point somewhere near ".bogus" (byte ~20), NOT 0..len
356        assert!(
357            span.start > 0,
358            "Error span should not start at 0, got {:?}",
359            span
360        );
361        assert!(
362            span.end < source.len(),
363            "Error span should not cover entire source, got {:?}",
364            span
365        );
366        // Verify the span actually points at ".bogus", not at "thing2"
367        let error_text = &source[span.start..span.end.min(source.len())];
368        assert!(
369            error_text.starts_with('.'),
370            "Error should point at '.bogus', but points at: '{}'",
371            &source[span.start..span.start + 10.min(source.len() - span.start)]
372        );
373    }
374
375    #[test]
376    fn test_assemble_with_equ_directive() {
377        let source = r#"
378        .globl entrypoint
379        .equ MY_CONST, 42
380        entrypoint:
381            mov64 r1, MY_CONST
382            exit
383        "#;
384        let result = assemble(source);
385        assert!(result.is_ok());
386    }
387
388    #[test]
389    fn test_assemble_duplicate_label_error() {
390        let source = r#"
391        .globl entrypoint
392        entrypoint:
393            mov64 r1, 1
394        entrypoint:
395            exit
396        "#;
397        let result = assemble(source);
398        assert!(result.is_err());
399        let errors = result.unwrap_err();
400        assert!(!errors.is_empty());
401    }
402
403    #[test]
404    fn test_assemble_extern_directive() {
405        let source = r#"
406        .globl entrypoint
407        .extern my_extern_symbol
408        entrypoint:
409            exit
410        "#;
411        let result = assemble(source);
412        assert!(result.is_ok());
413    }
414
415    #[test]
416    fn test_assemble_rodata_section() {
417        let source = r#"
418        .globl entrypoint
419        .rodata
420        my_data: .ascii "hello"
421        .text
422        entrypoint:
423            exit
424        "#;
425        let result = assemble(source);
426        assert!(result.is_ok());
427    }
428
429    #[test]
430    fn test_assemble_rodata_byte() {
431        let source = r#"
432        .globl entrypoint
433        .rodata
434        my_byte: .byte 0x42
435        .text
436        entrypoint:
437            exit
438        "#;
439        let result = assemble(source);
440        assert!(result.is_ok());
441    }
442
443    #[test]
444    fn test_assemble_rodata_multiple_bytes() {
445        let source = r#"
446        .globl entrypoint
447        .rodata
448        my_bytes: .byte 0x01, 0x02, 0x03, 0x04
449        .text
450        entrypoint:
451            exit
452        "#;
453        let result = assemble(source);
454        assert!(result.is_ok());
455    }
456
457    #[test]
458    fn test_assemble_rodata_mixed() {
459        let source = r#"
460        .globl entrypoint
461        .rodata
462        data1: .byte 0x42
463        data2: .ascii "test"
464        .text
465        entrypoint:
466            exit
467        "#;
468        let result = assemble(source);
469        assert!(result.is_ok());
470    }
471
472    #[test]
473    fn test_assemble_jump_operations() {
474        let source = r#"
475        .globl entrypoint
476        entrypoint:
477            jeq r1, 0, +1
478            ja +2
479        target:
480            jne r1, r2, target
481            exit
482        "#;
483        let result = assemble(source);
484        assert!(result.is_ok());
485    }
486
487    #[test]
488    fn test_assemble_offset_expression() {
489        let source = r#"
490        .globl entrypoint
491        .equ BASE, 100
492        entrypoint:
493            mov64 r1, BASE+10
494            exit
495        "#;
496        let result = assemble(source);
497        assert!(result.is_ok());
498    }
499
500    #[test]
501    fn test_assemble_equ_expression() {
502        let source = r#"
503        .globl entrypoint
504        .equ BASE, 100
505        .equ OFFSET, 20
506        .equ COMPUTED, BASE
507        entrypoint:
508            mov64 r1, BASE
509            mov64 r2, OFFSET
510            mov64 r3, COMPUTED
511            exit
512        "#;
513        let result = assemble(source);
514        assert!(result.is_ok());
515    }
516
517    #[test]
518    fn test_assemble_label_arithmetic_rodata_length() {
519        // The primary use case: compute string length via label subtraction
520        let source = r#"
521        .globl entrypoint
522        .rodata
523        msg: .ascii "Hello"
524        msg_end:
525        .text
526        entrypoint:
527            lddw r1, msg
528            mov64 r2, msg_end - msg
529            exit
530        "#;
531        let result = assemble(source);
532        assert!(result.is_ok(), "Failed: {:?}", result.err());
533    }
534
535    #[test]
536    fn test_assemble_label_arithmetic_with_offset() {
537        // Label arithmetic with additional constant offset
538        let source = r#"
539        .globl entrypoint
540        .rodata
541        msg: .ascii "Hello!"
542        msg_end:
543        .text
544        entrypoint:
545            lddw r1, msg
546            mov64 r2, msg_end - msg - 1
547            exit
548        "#;
549        let result = assemble(source);
550        assert!(result.is_ok(), "Failed: {:?}", result.err());
551    }
552
553    #[test]
554    fn test_assemble_label_arithmetic_text_section() {
555        // Label arithmetic works in the text section too
556        let source = r#"
557        .globl entrypoint
558        entrypoint:
559            mov64 r1, 1
560        middle:
561            mov64 r2, 2
562        end:
563            mov64 r3, end - entrypoint
564            exit
565        "#;
566        let result = assemble(source);
567        assert!(result.is_ok(), "Failed: {:?}", result.err());
568    }
569
570    #[test]
571    fn test_assemble_label_arithmetic_forward_reference() {
572        // Text section before rodata — forward references to rodata labels
573        let source = r#"
574        .globl entrypoint
575        entrypoint:
576            lddw r1, message
577            mov64 r2, message_end - message
578            call sol_log_
579            exit
580            lddw r10, 1
581        .rodata
582            message: .ascii "Hello, Solana!"
583            message_end:
584        "#;
585        let result = assemble(source);
586        assert!(
587            result.is_ok(),
588            "Forward reference failed: {:?}",
589            result.err()
590        );
591    }
592
593    #[test]
594    fn test_assemble_label_arithmetic_multiline_rodata() {
595        // Rodata label and directive on separate lines (as from macro expansion)
596        let source = r#"
597        .globl entrypoint
598        entrypoint:
599            lddw r1, message
600            mov64 r2, message_end - message
601            call sol_log_
602            exit
603        .rodata
604        message:
605            .ascii "Hello, Solana!"
606        message_end:
607        "#;
608        let result = assemble(source);
609        assert!(
610            result.is_ok(),
611            "Multi-line rodata failed: {:?}",
612            result.err()
613        );
614    }
615
616    #[test]
617    fn test_assemble_label_arithmetic_macro_e2e() {
618        // Full end-to-end test with macro expansion + label arithmetic
619        let source = r#"
620.macro DEF_STR name, text
621\name:
622    .ascii \text
623\name\()_end:
624.endm
625
626.macro SOL_LOG name
627    lddw r1, \name
628    mov64 r2, \name\()_end - \name
629    call sol_log_
630.endm
631
632.globl entrypoint
633entrypoint:
634    SOL_LOG message
635    exit
636.rodata
637    DEF_STR message, "Hello, Solana!"
638"#;
639        let assembler = Assembler::new(AssemblerOption::default());
640        let result = assembler.assemble_with_preprocess(source, "test.s", None);
641        assert!(result.is_ok(), "Macro e2e failed: {:?}", result.err());
642    }
643
644    #[test]
645    fn test_assemble_label_arithmetic_cross_section_error() {
646        // Cross-section arithmetic should fail
647        let source = r#"
648        .globl entrypoint
649        .rodata
650        msg: .ascii "Hello"
651        .text
652        entrypoint:
653            mov64 r1, msg - entrypoint
654            exit
655        "#;
656        let result = assemble(source);
657        assert!(result.is_err(), "Cross-section arithmetic should fail");
658    }
659
660    #[test]
661    fn test_assemble_label_arithmetic_complex_expression() {
662        // More complex expression with multiple rodata entries
663        let source = r#"
664        .globl entrypoint
665        .rodata
666        str1: .ascii "Hello"
667        str2: .ascii " World"
668        str2_end:
669        .text
670        entrypoint:
671            lddw r1, str2
672            mov64 r2, str2_end - str2
673            exit
674        "#;
675        let result = assemble(source);
676        assert!(result.is_ok(), "Failed: {:?}", result.err());
677    }
678
679    #[test]
680    fn test_parse_error_column_through_preprocess() {
681        // Verify the column offset is correctly computed through the
682        // preprocessing pipeline so errors point at the invalid token,
683        // not at the label before it.
684        let source = ".globl e\ne:\n    exit\n.rodata\n    thing1: .bogus 5\n";
685        let assembler = Assembler::new(AssemblerOption::default());
686        let result = assembler.assemble_with_preprocess(source, "test.s", None);
687        assert!(result.is_err());
688        let errors = result.unwrap_err();
689        let err = &errors.errors[0];
690        let origin = err.origin.as_ref().expect("Expected origin");
691        let registry = &errors.file_registry;
692
693        // Verify the column points at ".bogus", not "thing1"
694        let col = err.column.expect("Expected column info");
695        let line_start = registry.line_byte_offset(origin.file_id, origin.line);
696        let content = registry.content(origin.file_id);
697        let at_col = &content[line_start + col..];
698        assert!(
699            at_col.starts_with(".bogus"),
700            "Expected column to point at '.bogus', but points at: '{}'",
701            &at_col[..at_col.len().min(20)]
702        );
703    }
704
705    #[test]
706    fn test_parse_error_column_with_include_and_macros() {
707        // Simulate the user's actual scenario: .include with macros,
708        // then an invalid directive on a later line.
709        use std::collections::HashMap;
710
711        struct TestResolver {
712            files: HashMap<String, String>,
713        }
714        impl FileResolver for TestResolver {
715            fn resolve(&self, path: &str, _base: &str) -> Result<String, std::io::Error> {
716                self.files.get(path).cloned().ok_or_else(|| {
717                    std::io::Error::new(
718                        std::io::ErrorKind::NotFound,
719                        format!("not found: {}", path),
720                    )
721                })
722            }
723        }
724
725        let mut files = HashMap::new();
726        files.insert(
727            "syscalls/sol_log.s".to_string(),
728            r#".macro SOL_LOG name
729    lddw r1, \name
730    mov64 r2, \name\()_end - \name
731    call sol_log_
732.endm
733
734.macro DEF_STR name, text
735\name:
736    .ascii \text
737\name\()_end:
738.endm
739"#
740            .to_string(),
741        );
742
743        let source = r#".include "syscalls/sol_log.s"
744.globl e
745e:
746    SOL_LOG message
747.rodata
748    DEF_STR message, "TEST"
749    thing2: .int 1
750    thing1: .bogus "text"
751"#;
752        let resolver = TestResolver { files };
753        let assembler = Assembler::new(AssemblerOption::default());
754        let result = assembler.assemble_with_preprocess(source, "main.s", Some(&resolver));
755        assert!(result.is_err());
756        let errors = result.unwrap_err();
757        let err = &errors.errors[0];
758        let origin = err.origin.as_ref().expect("Expected origin");
759        let registry = &errors.file_registry;
760
761        // Verify origin points to main.s, not the included file
762        assert_eq!(registry.path(origin.file_id), "main.s");
763
764        // Verify the column points at ".bogus"
765        let col = err.column.expect("Expected column info");
766        let line_start = registry.line_byte_offset(origin.file_id, origin.line);
767        let content = registry.content(origin.file_id);
768        let at_col = &content[line_start + col..];
769        assert!(
770            at_col.starts_with(".bogus"),
771            "Expected column to point at '.bogus', but at line {} col {}, points at: '{}'",
772            origin.line,
773            col,
774            &at_col[..at_col.len().min(20)]
775        );
776    }
777
778    #[test]
779    fn test_assemble_with_debug_data() {
780        let source = r#".equ MSG_LEN, 14
781
782.globl entrypoint
783entrypoint:
784  lddw r1, message
785  mov64 r2, MSG_LEN
786  call sol_log_
787  exit
788.rodata
789  message: .ascii "Hello, Solana!"
790"#;
791        let result = assemble_with_debug_data(source, "hello_solana.s", "/tmp");
792        assert!(result.is_ok());
793        let bytecode = result.unwrap();
794
795        // Verify the ELF has all debug sections.
796        let bytecode_str = String::from_utf8_lossy(&bytecode);
797        assert!(
798            bytecode_str.contains(".debug_abbrev"),
799            "Missing .debug_abbrev section"
800        );
801        assert!(
802            bytecode_str.contains(".debug_info"),
803            "Missing .debug_info section"
804        );
805        assert!(
806            bytecode_str.contains(".debug_line"),
807            "Missing .debug_line section"
808        );
809        assert!(
810            bytecode_str.contains(".debug_line_str"),
811            "Missing .debug_line_str section"
812        );
813    }
814}