1use {anyhow::Result, codespan::Files};
2
3pub mod parser;
5
6pub mod preprocessor;
8
9pub mod errors;
11pub mod macros;
12
13pub mod ast;
15pub mod astnode;
16pub mod dynsym;
17
18pub mod header;
20pub mod program;
21pub mod section;
22
23pub mod debug;
25
26#[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#[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#[derive(Debug, Clone)]
65pub struct DebugMode {
66 pub filename: String,
68 pub directory: String,
70}
71
72#[derive(Debug, Clone, Default)]
74pub struct AssemblerOption {
75 pub arch: SbpfArch,
77 pub debug_mode: Option<DebugMode>,
79}
80
81#[derive(Debug)]
84pub struct AssemblerError {
85 pub error: CompileError,
86 pub origin: Option<SourceOrigin>,
87 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#[derive(Debug)]
107pub struct AssembleErrors {
108 pub errors: Vec<AssemblerError>,
109 pub file_registry: FileRegistry,
110}
111
112#[derive(Debug, Clone)]
114pub struct Assembler {
115 options: AssemblerOption,
116}
117
118impl Assembler {
119 pub fn new(options: AssemblerOption) -> Self {
121 Self { options }
122 }
123
124 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 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 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 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 let parse_result = match parse(expanded, self.options.arch) {
186 Ok(result) => result,
187 Err(errors) => {
188 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 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 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 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); type LabelEntry = (String, u64, u32); fn 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(registry.path(origin.file_id), "main.s");
763
764 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 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}