use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::graph::external_tools::{tool_detector, tool_invoker};
#[derive(Debug, thiserror::Error)]
pub enum ClangCompilationError {
#[error("Clang not found: {0}")]
ClangNotFound(String),
#[error("Compilation failed: {0}")]
CompilationFailed(String),
#[error("Unsupported file type: {extension}")]
UnsupportedFileType { extension: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceLanguage {
C,
Cpp,
ObjectiveC,
ObjectiveCpp,
}
impl SourceLanguage {
pub fn clang_flag(&self) -> &'static str {
match self {
SourceLanguage::C => "-xc",
SourceLanguage::Cpp => "-xc++",
SourceLanguage::ObjectiveC => "-xobjective-c",
SourceLanguage::ObjectiveCpp => "-xobjective-c++",
}
}
pub fn extensions() -> &'static [&'static str] {
&["c", "cpp", "cc", "cxx", "h", "hpp", "m", "mm"]
}
}
pub fn detect_source_language(path: &Path) -> Result<SourceLanguage, ClangCompilationError> {
let extension = path.extension().and_then(|e| e.to_str()).ok_or_else(|| {
ClangCompilationError::UnsupportedFileType {
extension: "<none>".to_string(),
}
})?;
match extension.to_lowercase().as_str() {
"c" => Ok(SourceLanguage::C),
"cpp" | "cc" | "cxx" | "hpp" => Ok(SourceLanguage::Cpp),
"m" => Ok(SourceLanguage::ObjectiveC),
"mm" => Ok(SourceLanguage::ObjectiveCpp),
_ => Err(ClangCompilationError::UnsupportedFileType {
extension: extension.to_string(),
}),
}
}
pub fn compile_to_llvm_ir(
source_path: &Path,
output_path: &Path,
) -> Result<(), ClangCompilationError> {
let clang_path = tool_detector::find_clang().map_err(|e| match e {
tool_detector::ToolDetectionError::ToolNotFound { .. } => {
ClangCompilationError::ClangNotFound("clang not found in PATH".to_string())
}
_ => ClangCompilationError::ClangNotFound(format!("clang detection failed: {}", e)),
})?;
let language = detect_source_language(source_path)?;
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| ClangCompilationError::Io(e))?;
}
let output = Command::new(&clang_path)
.arg(language.clang_flag()) .arg("-S") .arg("-emit-llvm") .arg("-o") .arg(output_path)
.arg(source_path)
.output()
.map_err(|e| ClangCompilationError::Io(e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ClangCompilationError::CompilationFailed(stderr.to_string()));
}
if !output_path.exists() {
return Err(ClangCompilationError::CompilationFailed(
"LLVM IR file was not created".to_string(),
));
}
Ok(())
}
pub fn compile_to_llvm_ir_temp(source_path: &Path) -> Result<PathBuf, ClangCompilationError> {
let source_stem = source_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
let temp_dir = std::env::temp_dir();
let output_path = temp_dir.join(format!("{}_{}.ll", source_stem, std::process::id()));
compile_to_llvm_ir(source_path, &output_path)?;
Ok(output_path)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_detect_source_language_c() {
assert_eq!(
detect_source_language(Path::new("test.c")).unwrap(),
SourceLanguage::C
);
}
#[test]
fn test_detect_source_language_cpp() {
assert_eq!(
detect_source_language(Path::new("test.cpp")).unwrap(),
SourceLanguage::Cpp
);
assert_eq!(
detect_source_language(Path::new("test.cc")).unwrap(),
SourceLanguage::Cpp
);
assert_eq!(
detect_source_language(Path::new("test.cxx")).unwrap(),
SourceLanguage::Cpp
);
}
#[test]
fn test_detect_source_language_header() {
assert!(detect_source_language(Path::new("test.h")).is_err());
assert_eq!(
detect_source_language(Path::new("test.hpp")).unwrap(),
SourceLanguage::Cpp
);
}
#[test]
fn test_detect_source_language_unsupported() {
assert!(detect_source_language(Path::new("test.rs")).is_err());
assert!(detect_source_language(Path::new("test.py")).is_err());
}
#[test]
fn test_clanguage_flag() {
assert_eq!(SourceLanguage::C.clang_flag(), "-xc");
assert_eq!(SourceLanguage::Cpp.clang_flag(), "-xc++");
assert_eq!(SourceLanguage::ObjectiveC.clang_flag(), "-xobjective-c");
assert_eq!(SourceLanguage::ObjectiveCpp.clang_flag(), "-xobjective-c++");
}
#[test]
fn test_compile_simple_c_function() {
if tool_detector::find_clang().is_err() {
return;
}
let source = r#"
int foo(int x) {
if (x > 0) {
return x * 2;
} else {
return x + 1;
}
}
"#;
let mut temp_file = NamedTempFile::with_suffix(".c").unwrap();
temp_file.write_all(source.as_bytes()).unwrap();
let source_path = temp_file.path();
let output_path = compile_to_llvm_ir_temp(source_path);
assert!(output_path.is_ok());
let output_path = output_path.unwrap();
assert!(output_path.exists());
let content = std::fs::read_to_string(&output_path).unwrap();
assert!(
content.contains("define i32 @foo") || content.contains("define dso_local i32 @foo")
);
assert!(content.contains("{") && content.contains("}"));
let _ = std::fs::remove_file(output_path);
}
#[test]
fn test_compile_simple_cpp_function() {
if tool_detector::find_clang().is_err() {
return;
}
let source = r#"
extern "C" int bar(int x) {
return x + 42;
}
"#;
let mut temp_file = NamedTempFile::with_suffix(".cpp").unwrap();
temp_file.write_all(source.as_bytes()).unwrap();
let source_path = temp_file.path();
let output_path = compile_to_llvm_ir_temp(source_path);
assert!(output_path.is_ok());
let output_path = output_path.unwrap();
assert!(output_path.exists());
let content = std::fs::read_to_string(&output_path).unwrap();
assert!(
content.contains("define i32 @bar") || content.contains("define dso_local i32 @bar")
);
assert!(content.contains("{") && content.contains("}"));
let _ = std::fs::remove_file(output_path);
}
}