1#[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#[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#[derive(Debug, Clone)]
63pub struct CompileOptions {
64 pub opt_level: u8,
66 pub debug_info: bool,
68 pub output_dir: PathBuf,
70 pub use_cargo_sbf: bool,
72 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#[derive(Debug)]
90pub struct CompileResult {
91 pub program_path: PathBuf,
93 pub program_id: Option<String>,
95 pub build_time_secs: f64,
97}
98
99pub 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
119fn compile_via_anchor(
121 program: &Program,
122 source: &str,
123 options: &CompileOptions,
124 start: std::time::Instant,
125) -> Result<CompileResult> {
126 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 let generated =
134 solscript_codegen::generate(program).map_err(|e| BpfError::CodegenError(e.to_string()))?;
135
136 let anchor_dir = options.output_dir.join("anchor_project");
138 generated
139 .write_to_dir(&anchor_dir)
140 .map_err(BpfError::IoError)?;
141
142 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 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 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 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 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 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 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 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
243fn 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 None
250 } else {
251 None
252 }
253}
254
255#[cfg(feature = "llvm")]
259fn link_bpf_object(obj_path: &Path, so_path: &Path) -> Result<()> {
260 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 if let Ok(meta) = std::fs::metadata(so_path) {
277 if meta.len() > 500 {
278 return Ok(());
280 }
281 }
282 }
284 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 eprintln!("Warning: sbpf-linker reported: {}", stderr.trim());
290 }
291 }
292 }
293
294 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 Target::initialize_bpf(&InitializationConfig::default());
314
315 let context = Context::create();
316 let module = context.create_module("solscript_program");
317
318 let mut compiler = Compiler::new(&context, &module);
320 compiler.compile_program(program)?;
321
322 if let Err(msg) = module.verify() {
324 return Err(BpfError::LlvmError(msg.to_string()));
325 }
326
327 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 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 let so_path = options.output_dir.join("solscript_program.so");
359 link_bpf_object(&obj_path, &so_path)?;
360
361 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
373pub 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#[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 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 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}