solscript_bpf/
lib.rs

1//! BPF Compilation for SolScript
2//!
3//! This module provides BPF bytecode compilation for SolScript programs.
4//!
5//! Two compilation modes are supported:
6//! 1. **Standard mode** (default): Uses Rust/Anchor codegen + cargo build-sbf
7//! 2. **Direct LLVM mode** (feature: `llvm`): Compiles directly to BPF via LLVM
8//!
9//! The standard mode is recommended for most use cases as it leverages the
10//! well-tested Anchor framework. Direct LLVM mode provides faster compilation
11//! but requires LLVM 18 with Polly support.
12
13#[cfg(feature = "llvm")]
14mod codegen;
15#[cfg(feature = "llvm")]
16mod intrinsics;
17#[cfg(feature = "llvm")]
18mod types;
19
20use solscript_ast::Program;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23use thiserror::Error;
24
25#[cfg(feature = "llvm")]
26pub use codegen::Compiler;
27
28/// Errors that can occur during BPF compilation
29#[derive(Debug, Error)]
30pub enum BpfError {
31    #[error("Codegen error: {0}")]
32    CodegenError(String),
33
34    #[error("Build error: {0}")]
35    BuildError(String),
36
37    #[error("IO error: {0}")]
38    IoError(#[from] std::io::Error),
39
40    #[error("Tool not found: {0}")]
41    ToolNotFound(String),
42
43    #[cfg(feature = "llvm")]
44    #[error("LLVM error: {0}")]
45    LlvmError(String),
46
47    #[cfg(feature = "llvm")]
48    #[error("Target error: {0}")]
49    TargetError(String),
50
51    #[cfg(feature = "llvm")]
52    #[error("Unsupported feature: {0}")]
53    Unsupported(String),
54
55    #[error("Linker error: {0}")]
56    LinkerError(String),
57}
58
59pub type Result<T> = std::result::Result<T, BpfError>;
60
61/// BPF compilation options
62#[derive(Debug, Clone)]
63pub struct CompileOptions {
64    /// Optimization level (0-3)
65    pub opt_level: u8,
66    /// Generate debug information
67    pub debug_info: bool,
68    /// Output directory
69    pub output_dir: PathBuf,
70    /// Use cargo build-sbf (standard mode)
71    pub use_cargo_sbf: bool,
72    /// Keep intermediate files
73    pub keep_intermediate: bool,
74}
75
76impl Default for CompileOptions {
77    fn default() -> Self {
78        Self {
79            opt_level: 2,
80            debug_info: false,
81            output_dir: PathBuf::from("target/deploy"),
82            use_cargo_sbf: true,
83            keep_intermediate: false,
84        }
85    }
86}
87
88/// BPF compilation result
89#[derive(Debug)]
90pub struct CompileResult {
91    /// Path to the compiled .so file
92    pub program_path: PathBuf,
93    /// Program ID (if available)
94    pub program_id: Option<String>,
95    /// Build duration in seconds
96    pub build_time_secs: f64,
97}
98
99/// Compile a SolScript program to BPF
100pub fn compile(program: &Program, source: &str, options: &CompileOptions) -> Result<CompileResult> {
101    let start = std::time::Instant::now();
102
103    if options.use_cargo_sbf {
104        compile_via_anchor(program, source, options, start)
105    } else {
106        #[cfg(feature = "llvm")]
107        {
108            compile_direct_llvm(program, options, start)
109        }
110        #[cfg(not(feature = "llvm"))]
111        {
112            Err(BpfError::BuildError(
113                "Direct LLVM compilation requires the 'llvm' feature".to_string(),
114            ))
115        }
116    }
117}
118
119/// Compile via Anchor/cargo build-sbf (standard mode)
120fn compile_via_anchor(
121    program: &Program,
122    source: &str,
123    options: &CompileOptions,
124    start: std::time::Instant,
125) -> Result<CompileResult> {
126    // First, type check
127    if let Err(errors) = solscript_typeck::typecheck(program, source) {
128        let msgs: Vec<_> = errors.iter().map(|e| e.to_string()).collect();
129        return Err(BpfError::CodegenError(msgs.join("\n")));
130    }
131
132    // Generate Anchor code
133    let generated =
134        solscript_codegen::generate(program).map_err(|e| BpfError::CodegenError(e.to_string()))?;
135
136    // Write to output directory
137    let anchor_dir = options.output_dir.join("anchor_project");
138    generated
139        .write_to_dir(&anchor_dir)
140        .map_err(BpfError::IoError)?;
141
142    // Check if cargo build-sbf is available
143    let build_sbf_available = Command::new("cargo")
144        .args(["build-sbf", "--version"])
145        .output()
146        .map(|o| o.status.success())
147        .unwrap_or(false);
148
149    if !build_sbf_available {
150        // Try cargo build-bpf (older command)
151        let build_bpf_available = Command::new("cargo")
152            .args(["build-bpf", "--version"])
153            .output()
154            .map(|o| o.status.success())
155            .unwrap_or(false);
156
157        if !build_bpf_available {
158            return Err(BpfError::ToolNotFound(
159                "cargo build-sbf (or cargo build-bpf) not found. \
160                 Install with: cargo install solana-cli"
161                    .to_string(),
162            ));
163        }
164    }
165
166    // Run cargo build-sbf
167    let build_cmd = if build_sbf_available {
168        "build-sbf"
169    } else {
170        "build-bpf"
171    };
172
173    let program_dir = anchor_dir.join("programs").join("solscript_program");
174
175    let mut cmd = Command::new("cargo");
176    cmd.arg(build_cmd);
177
178    // Add optimization flags
179    match options.opt_level {
180        0 => {}
181        1 => {
182            cmd.env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "1");
183        }
184        2 => {
185            cmd.env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "2");
186        }
187        _ => {
188            cmd.env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3");
189        }
190    }
191
192    cmd.current_dir(&program_dir);
193
194    let output = cmd
195        .output()
196        .map_err(|e| BpfError::BuildError(format!("Failed to run {}: {}", build_cmd, e)))?;
197
198    if !output.status.success() {
199        let stderr = String::from_utf8_lossy(&output.stderr);
200        return Err(BpfError::BuildError(format!("Build failed:\n{}", stderr)));
201    }
202
203    // Find the compiled .so file
204    let deploy_dir = anchor_dir.join("target/deploy");
205    let so_path = deploy_dir.join("solscript_program.so");
206
207    if !so_path.exists() {
208        // Try alternative path
209        let alt_path = program_dir.join("target/deploy/solscript_program.so");
210        if alt_path.exists() {
211            let final_path = options.output_dir.join("solscript_program.so");
212            std::fs::copy(&alt_path, &final_path)?;
213
214            return Ok(CompileResult {
215                program_path: final_path,
216                program_id: read_program_id(&program_dir),
217                build_time_secs: start.elapsed().as_secs_f64(),
218            });
219        }
220
221        return Err(BpfError::BuildError(
222            "Compiled program not found".to_string(),
223        ));
224    }
225
226    // Copy to output directory
227    let final_path = options.output_dir.join("solscript_program.so");
228    std::fs::create_dir_all(&options.output_dir)?;
229    std::fs::copy(&so_path, &final_path)?;
230
231    // Clean up if not keeping intermediate files
232    if !options.keep_intermediate {
233        let _ = std::fs::remove_dir_all(&anchor_dir);
234    }
235
236    Ok(CompileResult {
237        program_path: final_path,
238        program_id: read_program_id(&program_dir),
239        build_time_secs: start.elapsed().as_secs_f64(),
240    })
241}
242
243/// Read program ID from the keypair file
244fn read_program_id(program_dir: &Path) -> Option<String> {
245    let keypair_path = program_dir.join("target/deploy/solscript_program-keypair.json");
246    if keypair_path.exists() {
247        // The keypair file contains the program ID
248        // For now, just return None - we'd need to parse the keypair
249        None
250    } else {
251        None
252    }
253}
254
255/// Link a BPF object file to create a shared object (.so)
256///
257/// Tries sbpf-linker first (preferred for Solana), then falls back to lld-18
258#[cfg(feature = "llvm")]
259fn link_bpf_object(obj_path: &Path, so_path: &Path) -> Result<()> {
260    // Try sbpf-linker first (preferred for Solana programs)
261    let sbpf_result = Command::new("sbpf-linker")
262        .args([
263            "--cpu",
264            "v3",
265            "--output",
266            so_path.to_str().unwrap(),
267            "--export",
268            "entrypoint",
269            obj_path.to_str().unwrap(),
270        ])
271        .output();
272
273    if let Ok(output) = sbpf_result {
274        if output.status.success() {
275            // Check if the output file has content (sbpf-linker sometimes produces empty files)
276            if let Ok(meta) = std::fs::metadata(so_path) {
277                if meta.len() > 500 {
278                    // Sanity check: valid linked output should be larger than 500 bytes
279                    return Ok(());
280                }
281            }
282            // Fall through to use object file directly
283        }
284        // If sbpf-linker exists but failed, report the error
285        if !String::from_utf8_lossy(&output.stderr).contains("not found") {
286            let stderr = String::from_utf8_lossy(&output.stderr);
287            if !stderr.is_empty() && stderr.trim().len() > 0 {
288                // Don't fail, just log and continue to fallback
289                eprintln!("Warning: sbpf-linker reported: {}", stderr.trim());
290            }
291        }
292    }
293
294    // Fallback: Use the object file directly
295    // Solana's program loader can handle relocatable object files
296    std::fs::copy(obj_path, so_path)?;
297    Ok(())
298}
299
300#[cfg(feature = "llvm")]
301fn compile_direct_llvm(
302    program: &Program,
303    options: &CompileOptions,
304    start: std::time::Instant,
305) -> Result<CompileResult> {
306    use inkwell::context::Context;
307    use inkwell::targets::{
308        CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetTriple,
309    };
310    use inkwell::OptimizationLevel;
311
312    // Initialize BPF target
313    Target::initialize_bpf(&InitializationConfig::default());
314
315    let context = Context::create();
316    let module = context.create_module("solscript_program");
317
318    // Compile to LLVM IR
319    let mut compiler = Compiler::new(&context, &module);
320    compiler.compile_program(program)?;
321
322    // Verify module
323    if let Err(msg) = module.verify() {
324        return Err(BpfError::LlvmError(msg.to_string()));
325    }
326
327    // Set up BPF target
328    let triple = TargetTriple::create("bpfel-unknown-none");
329    let target = Target::from_triple(&triple).map_err(|e| BpfError::TargetError(e.to_string()))?;
330
331    let opt = match options.opt_level {
332        0 => OptimizationLevel::None,
333        1 => OptimizationLevel::Less,
334        2 => OptimizationLevel::Default,
335        _ => OptimizationLevel::Aggressive,
336    };
337
338    let target_machine = target
339        .create_target_machine(
340            &triple,
341            "generic",
342            "",
343            opt,
344            RelocMode::PIC,
345            CodeModel::Default,
346        )
347        .ok_or_else(|| BpfError::TargetError("Failed to create target machine".to_string()))?;
348
349    // Emit object file
350    std::fs::create_dir_all(&options.output_dir)?;
351    let obj_path = options.output_dir.join("solscript_program.o");
352
353    target_machine
354        .write_to_file(&module, FileType::Object, &obj_path)
355        .map_err(|e| BpfError::LlvmError(e.to_string()))?;
356
357    // Link to create .so
358    let so_path = options.output_dir.join("solscript_program.so");
359    link_bpf_object(&obj_path, &so_path)?;
360
361    // Clean up object file if not keeping intermediate files
362    if !options.keep_intermediate {
363        let _ = std::fs::remove_file(&obj_path);
364    }
365
366    Ok(CompileResult {
367        program_path: so_path,
368        program_id: None,
369        build_time_secs: start.elapsed().as_secs_f64(),
370    })
371}
372
373/// Check if BPF build tools are available
374pub fn check_tools() -> Result<ToolStatus> {
375    let cargo_sbf = Command::new("cargo")
376        .args(["build-sbf", "--version"])
377        .output()
378        .map(|o| {
379            if o.status.success() {
380                Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
381            } else {
382                None
383            }
384        })
385        .unwrap_or(None);
386
387    let cargo_bpf = Command::new("cargo")
388        .args(["build-bpf", "--version"])
389        .output()
390        .map(|o| {
391            if o.status.success() {
392                Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
393            } else {
394                None
395            }
396        })
397        .unwrap_or(None);
398
399    let solana_cli = Command::new("solana")
400        .args(["--version"])
401        .output()
402        .map(|o| {
403            if o.status.success() {
404                Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
405            } else {
406                None
407            }
408        })
409        .unwrap_or(None);
410
411    let anchor = Command::new("anchor")
412        .args(["--version"])
413        .output()
414        .map(|o| {
415            if o.status.success() {
416                Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
417            } else {
418                None
419            }
420        })
421        .unwrap_or(None);
422
423    Ok(ToolStatus {
424        cargo_build_sbf: cargo_sbf,
425        cargo_build_bpf: cargo_bpf,
426        solana_cli,
427        anchor,
428        #[cfg(feature = "llvm")]
429        llvm_available: check_llvm(),
430        #[cfg(not(feature = "llvm"))]
431        llvm_available: false,
432    })
433}
434
435#[cfg(feature = "llvm")]
436fn check_llvm() -> bool {
437    use inkwell::targets::{InitializationConfig, Target};
438    Target::initialize_bpf(&InitializationConfig::default());
439    Target::from_name("bpf").is_some()
440}
441
442/// Status of available build tools
443#[derive(Debug)]
444pub struct ToolStatus {
445    pub cargo_build_sbf: Option<String>,
446    pub cargo_build_bpf: Option<String>,
447    pub solana_cli: Option<String>,
448    pub anchor: Option<String>,
449    pub llvm_available: bool,
450}
451
452impl ToolStatus {
453    /// Check if any BPF build method is available
454    pub fn can_build(&self) -> bool {
455        self.cargo_build_sbf.is_some() || self.cargo_build_bpf.is_some() || self.llvm_available
456    }
457
458    /// Get a summary of available tools
459    pub fn summary(&self) -> String {
460        let mut lines = Vec::new();
461
462        if let Some(v) = &self.cargo_build_sbf {
463            lines.push(format!("✓ cargo build-sbf: {}", v));
464        } else if let Some(v) = &self.cargo_build_bpf {
465            lines.push(format!("✓ cargo build-bpf: {}", v));
466        } else {
467            lines.push("✗ cargo build-sbf: not found".to_string());
468        }
469
470        if let Some(v) = &self.solana_cli {
471            lines.push(format!("✓ solana: {}", v));
472        } else {
473            lines.push("✗ solana: not found".to_string());
474        }
475
476        if let Some(v) = &self.anchor {
477            lines.push(format!("✓ anchor: {}", v));
478        } else {
479            lines.push("✗ anchor: not found".to_string());
480        }
481
482        if self.llvm_available {
483            lines.push("✓ LLVM BPF target: available".to_string());
484        }
485
486        lines.join("\n")
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    #[test]
495    fn test_default_options() {
496        let opts = CompileOptions::default();
497        assert_eq!(opts.opt_level, 2);
498        assert!(!opts.debug_info);
499        assert!(opts.use_cargo_sbf);
500    }
501
502    #[test]
503    fn test_tool_status_can_build() {
504        let status = ToolStatus {
505            cargo_build_sbf: Some("1.0".to_string()),
506            cargo_build_bpf: None,
507            solana_cli: None,
508            anchor: None,
509            llvm_available: false,
510        };
511        assert!(status.can_build());
512    }
513}