use crate::converge::type_constraint_learner::{parse_e0308_constraint, TypeConstraintStore};
use anyhow::{Context, Result};
use depyler_core::DepylerPipeline;
use indicatif::{ProgressBar, ProgressStyle};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const MAX_ORACLE_RETRIES: usize = 2;
pub fn compile_python_to_binary(
input: &Path,
output: Option<&Path>,
profile: Option<&str>,
) -> Result<PathBuf> {
if !input.exists() {
anyhow::bail!("Input file not found: {}", input.display());
}
let pb = ProgressBar::new(4);
pb.set_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}")
.expect("static progress template")
.progress_chars("█▓▒░ "),
);
let python_code = fs::read_to_string(input)
.with_context(|| format!("Failed to read input file: {}", input.display()))?;
let cargo_profile = profile.unwrap_or("release");
let mut constraint_store = TypeConstraintStore::new();
let mut last_error: Option<String> = None;
for attempt in 0..=MAX_ORACLE_RETRIES {
pb.set_message(if attempt == 0 {
"Transpiling Python to Rust...".to_string()
} else {
format!("Re-transpiling (attempt {})...", attempt + 1)
});
let pipeline = DepylerPipeline::new();
let (rust_code, dependencies) = if constraint_store.stats.constraints_extracted > 0 {
let input_str = input.to_string_lossy().to_string();
let constraints_map: HashMap<String, String> = constraint_store
.variable_constraints
.iter()
.filter(|((file, _), _)| file == &input_str)
.map(|((_, var), constraint)| (var.clone(), constraint.expected_type.clone()))
.collect();
pipeline
.transpile_with_constraints_and_dependencies(&python_code, &constraints_map)
.context("Failed to transpile with constraints")?
} else {
pipeline
.transpile_with_dependencies(&python_code)
.context("Failed to transpile Python to Rust")?
};
if attempt == 0 {
pb.inc(1);
}
pb.set_message("Creating Cargo project...");
let (project_dir, is_binary) = create_cargo_project(input, &rust_code, &dependencies)?;
if attempt == 0 {
pb.inc(1);
}
pb.set_message(if is_binary {
"Building binary...".to_string()
} else {
"Building library...".to_string()
});
let build_result = build_cargo_project(&project_dir, cargo_profile)?;
if build_result.success {
if attempt == 0 {
pb.inc(1);
}
pb.set_message("Finalizing...");
let result_path = if is_binary {
finalize_binary(&project_dir, input, output, cargo_profile)?
} else {
project_dir.clone()
};
pb.inc(1);
let success_msg = if attempt > 0 {
format!(
"✅ Compilation complete (after {} Oracle Loop retries)!",
attempt
)
} else if is_binary {
"✅ Compilation complete!".to_string()
} else {
"✅ Library compilation complete!".to_string()
};
pb.finish_with_message(success_msg);
if constraint_store.stats.constraints_extracted > 0 {
tracing::info!(
"DEPYLER-1102: Oracle Loop learned {} type constraints",
constraint_store.stats.constraints_extracted
);
}
return Ok(result_path);
}
let new_constraints = extract_e0308_constraints(&build_result.stderr, input);
if new_constraints.stats.constraints_extracted > 0 && attempt < MAX_ORACLE_RETRIES {
tracing::info!(
"DEPYLER-1102: Extracted {} E0308 constraints, retrying...",
new_constraints.stats.constraints_extracted
);
for (key, constraint) in new_constraints.variable_constraints {
constraint_store
.variable_constraints
.insert(key, constraint);
}
constraint_store.stats.constraints_extracted +=
new_constraints.stats.constraints_extracted;
continue;
}
last_error = Some(build_result.stderr);
break;
}
pb.finish_with_message("❌ Compilation failed");
anyhow::bail!(
"Cargo build failed after {} attempts:\n{}",
MAX_ORACLE_RETRIES + 1,
last_error.unwrap_or_else(|| "Unknown error".to_string())
)
}
fn create_cargo_project(
input: &Path,
rust_code: &str,
dependencies: &[depyler_core::cargo_toml_gen::Dependency],
) -> Result<(PathBuf, bool)> {
let project_name = input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
let temp_dir = std::env::temp_dir();
let project_dir = temp_dir.join(format!("depyler_{}", project_name));
let src_dir = project_dir.join("src");
if src_dir.exists() {
fs::remove_dir_all(&src_dir).ok(); }
fs::create_dir_all(&src_dir).context("Failed to create src directory")?;
let has_main = rust_code.contains("fn main()") || rust_code.contains("pub fn main()");
let (rs_filename, cargo_toml) = if has_main {
let toml = depyler_core::cargo_toml_gen::generate_cargo_toml(
project_name,
"src/main.rs",
dependencies,
);
("src/main.rs", toml)
} else {
let toml = depyler_core::cargo_toml_gen::generate_cargo_toml_lib(
project_name,
"src/lib.rs",
dependencies,
);
("src/lib.rs", toml)
};
fs::write(project_dir.join("Cargo.toml"), cargo_toml).context("Failed to write Cargo.toml")?;
fs::write(project_dir.join(rs_filename), rust_code)
.with_context(|| format!("Failed to write {}", rs_filename))?;
Ok((project_dir, has_main))
}
#[derive(Debug)]
pub struct BuildResult {
pub success: bool,
pub stderr: String,
}
fn build_cargo_project(project_dir: &Path, profile: &str) -> Result<BuildResult> {
let mut cmd = Command::new("cargo");
cmd.arg("build")
.arg("--manifest-path")
.arg(project_dir.join("Cargo.toml"))
.arg("--target-dir")
.arg(project_dir.join("target"));
if profile == "release" {
cmd.arg("--release");
}
let output = cmd.output().context("Failed to run cargo build")?;
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok(BuildResult {
success: output.status.success(),
stderr,
})
}
fn extract_e0308_constraints(stderr: &str, source_file: &Path) -> TypeConstraintStore {
let mut store = TypeConstraintStore::new();
for line in stderr.lines() {
if line.contains("error[E0308]") {
if let Some(msg_start) = line.find("]: ") {
let message = &line[msg_start + 3..];
if let Some(constraint) = parse_e0308_constraint(message, source_file, 0) {
store.add_constraint(constraint);
}
}
}
}
for line in stderr.lines() {
if (line.contains("expected `") && line.contains("found `"))
|| line.contains("expected type")
{
if let Some(constraint) = parse_e0308_constraint(line, source_file, 0) {
store.add_constraint(constraint);
}
}
}
store
}
fn finalize_binary(
project_dir: &Path,
input: &Path,
output: Option<&Path>,
profile: &str,
) -> Result<PathBuf> {
let project_name = input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
let profile_dir = if profile == "release" {
"release"
} else {
"debug"
};
let binary_name = if cfg!(windows) {
format!("{}.exe", project_name)
} else {
project_name.to_string()
};
let built_binary = project_dir
.join("target")
.join(profile_dir)
.join(&binary_name);
let output_path = if let Some(out) = output {
out.to_path_buf()
} else {
input.with_file_name(&binary_name)
};
fs::copy(&built_binary, &output_path).with_context(|| {
format!(
"Failed to copy binary from {} to {}",
built_binary.display(),
output_path.display()
)
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&output_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&output_path, perms)?;
}
Ok(output_path)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_create_cargo_project_binary() {
let rust_code = r#"fn main() { println!("test"); }"#;
let temp = TempDir::new().unwrap();
let input = temp.path().join("test.py");
fs::write(&input, "").unwrap();
let dependencies = vec![];
let (project_dir, is_binary) =
create_cargo_project(&input, rust_code, &dependencies).unwrap();
assert!(
is_binary,
"Code with fn main() should be detected as binary"
);
assert!(project_dir.join("Cargo.toml").exists());
assert!(project_dir.join("src/main.rs").exists());
let main_content = fs::read_to_string(project_dir.join("src/main.rs")).unwrap();
assert!(main_content.contains("test"));
let cargo_content = fs::read_to_string(project_dir.join("Cargo.toml")).unwrap();
assert!(cargo_content.contains("[package]"));
assert!(cargo_content.contains("name = \"test\""));
}
#[test]
fn test_create_cargo_project_pub_main() {
let rust_code = r#"pub fn main() { println!("public main"); }"#;
let temp = TempDir::new().unwrap();
let input = temp.path().join("pub_main.py");
fs::write(&input, "").unwrap();
let dependencies = vec![];
let (_, is_binary) = create_cargo_project(&input, rust_code, &dependencies).unwrap();
assert!(
is_binary,
"Code with pub fn main() should be detected as binary"
);
}
#[test]
fn test_create_cargo_project_library() {
let rust_code = r#"pub fn greet(name: &str) -> String { format!("Hello, {}!", name) }"#;
let temp = TempDir::new().unwrap();
let input = temp.path().join("mylib.py");
fs::write(&input, "").unwrap();
let dependencies = vec![];
let (project_dir, is_binary) =
create_cargo_project(&input, rust_code, &dependencies).unwrap();
assert!(
!is_binary,
"Code without fn main() should be detected as library"
);
assert!(project_dir.join("Cargo.toml").exists());
assert!(project_dir.join("src/lib.rs").exists());
assert!(
!project_dir.join("src/main.rs").exists(),
"Library should not have main.rs"
);
let cargo_content = fs::read_to_string(project_dir.join("Cargo.toml")).unwrap();
assert!(
cargo_content.contains("[lib]"),
"Library should have [lib] section"
);
assert!(
!cargo_content.contains("[[bin]]"),
"Library should not have [[bin]] section"
);
}
#[test]
fn test_create_cargo_project_with_dependencies() {
use depyler_core::cargo_toml_gen::Dependency;
let rust_code = r#"fn main() { println!("test"); }"#;
let temp = TempDir::new().unwrap();
let input = temp.path().join("test_deps.py");
fs::write(&input, "").unwrap();
let dependencies = vec![
Dependency {
crate_name: "serde".to_string(),
version: "1.0".to_string(),
features: vec!["derive".to_string()],
},
Dependency {
crate_name: "regex".to_string(),
version: "1.0".to_string(),
features: vec![],
},
];
let (project_dir, _) = create_cargo_project(&input, rust_code, &dependencies).unwrap();
let cargo_content = fs::read_to_string(project_dir.join("Cargo.toml")).unwrap();
assert!(cargo_content.contains("serde"));
assert!(cargo_content.contains("regex"));
}
#[test]
fn test_create_cargo_project_cleanup_existing() {
let rust_code = r#"fn main() { println!("new"); }"#;
let temp = TempDir::new().unwrap();
let input = temp.path().join("cleanup_test.py");
fs::write(&input, "").unwrap();
let dependencies = vec![];
let (project_dir, _) = create_cargo_project(&input, rust_code, &dependencies).unwrap();
assert!(project_dir.join("src/main.rs").exists());
fs::write(project_dir.join("src/stale.rs"), "stale content").unwrap();
let lib_code = r#"pub fn greet() -> &'static str { "hello" }"#;
let (project_dir2, _) = create_cargo_project(&input, lib_code, &dependencies).unwrap();
assert_eq!(project_dir, project_dir2);
assert!(
!project_dir.join("src/stale.rs").exists(),
"Stale files should be cleaned"
);
assert!(
!project_dir.join("src/main.rs").exists(),
"main.rs should be removed for library"
);
assert!(
project_dir.join("src/lib.rs").exists(),
"lib.rs should exist"
);
}
#[test]
fn test_compile_nonexistent_file() {
let result =
compile_python_to_binary(Path::new("/nonexistent/file.py"), None, Some("release"));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
fn test_build_cargo_project_release() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().to_path_buf();
let src_dir = project_dir.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(
src_dir.join("main.rs"),
r#"fn main() { println!("test"); }"#,
)
.unwrap();
let cargo_toml = r#"
[package]
name = "test_build"
version = "0.1.0"
edition = "2021"
"#;
fs::write(project_dir.join("Cargo.toml"), cargo_toml).unwrap();
let result = build_cargo_project(&project_dir, "release");
assert!(result.is_ok());
assert!(project_dir.join("target/release/test_build").exists());
}
#[test]
fn test_build_cargo_project_debug() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().to_path_buf();
let src_dir = project_dir.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("main.rs"), r#"fn main() { }"#).unwrap();
let cargo_toml = r#"
[package]
name = "test_debug"
version = "0.1.0"
edition = "2021"
"#;
fs::write(project_dir.join("Cargo.toml"), cargo_toml).unwrap();
let result = build_cargo_project(&project_dir, "debug");
assert!(result.is_ok());
assert!(project_dir.join("target/debug/test_debug").exists());
}
#[test]
fn test_build_cargo_project_invalid_code() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().to_path_buf();
let src_dir = project_dir.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("main.rs"), "this is not valid rust").unwrap();
let cargo_toml = r#"
[package]
name = "test_invalid"
version = "0.1.0"
edition = "2021"
"#;
fs::write(project_dir.join("Cargo.toml"), cargo_toml).unwrap();
let result = build_cargo_project(&project_dir, "release").unwrap();
assert!(!result.success, "Invalid code should fail to compile");
assert!(!result.stderr.is_empty(), "Should have error output");
}
#[test]
fn test_extract_e0308_constraints_basic() {
let source = Path::new("test.py");
let stderr = r#"
error[E0308]: mismatched types
--> src/main.rs:10:5
|
10 | x
| ^ expected `String`, found `i64`
"#;
let store = extract_e0308_constraints(stderr, source);
assert!(
store.stats.constraints_extracted > 0,
"Should extract E0308 constraint"
);
}
#[test]
fn test_extract_e0308_constraints_multiple() {
let source = Path::new("test.py");
let stderr = r#"
error[E0308]: mismatched types
--> src/main.rs:10:5
|
10 | x
| ^ expected `String`, found `i64`
error[E0308]: mismatched types
--> src/main.rs:20:5
|
20 | y
| ^ expected `f64`, found `bool`
"#;
let store = extract_e0308_constraints(stderr, source);
assert!(
store.stats.constraints_extracted >= 2,
"Should extract multiple E0308 constraints"
);
}
#[test]
fn test_extract_e0308_constraints_no_e0308() {
let source = Path::new("test.py");
let stderr = r#"
error[E0425]: cannot find value `foo` in this scope
--> src/main.rs:5:5
|
5 | foo
| ^^^ not found in this scope
"#;
let store = extract_e0308_constraints(stderr, source);
assert_eq!(
store.stats.constraints_extracted, 0,
"Should not extract non-E0308 errors"
);
}
#[test]
fn test_build_result_success_true() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().to_path_buf();
let src_dir = project_dir.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("main.rs"), r#"fn main() {}"#).unwrap();
let cargo_toml = r#"
[package]
name = "test_valid"
version = "0.1.0"
edition = "2021"
"#;
fs::write(project_dir.join("Cargo.toml"), cargo_toml).unwrap();
let result = build_cargo_project(&project_dir, "release").unwrap();
assert!(result.success, "Valid code should compile successfully");
}
#[test]
fn test_finalize_binary_default_output() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("project");
let target_release = project_dir.join("target/release");
fs::create_dir_all(&target_release).unwrap();
fs::write(target_release.join("test_final"), "binary content").unwrap();
let input = temp.path().join("test_final.py");
fs::write(&input, "").unwrap();
let result = finalize_binary(&project_dir, &input, None, "release");
assert!(result.is_ok());
let output_path = result.unwrap();
assert!(output_path.exists());
assert!(output_path.to_string_lossy().contains("test_final"));
}
#[test]
fn test_finalize_binary_custom_output() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("project");
let target_release = project_dir.join("target/release");
fs::create_dir_all(&target_release).unwrap();
fs::write(target_release.join("custom_name"), "binary content").unwrap();
let input = temp.path().join("custom_name.py");
fs::write(&input, "").unwrap();
let custom_output = temp.path().join("my_custom_binary");
let result = finalize_binary(&project_dir, &input, Some(&custom_output), "release");
assert!(result.is_ok());
let output_path = result.unwrap();
assert_eq!(output_path, custom_output);
assert!(output_path.exists());
}
#[test]
fn test_finalize_binary_debug_profile() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("project");
let target_debug = project_dir.join("target/debug");
fs::create_dir_all(&target_debug).unwrap();
fs::write(target_debug.join("debug_test"), "binary content").unwrap();
let input = temp.path().join("debug_test.py");
fs::write(&input, "").unwrap();
let result = finalize_binary(&project_dir, &input, None, "debug");
assert!(result.is_ok());
}
#[cfg(unix)]
#[test]
fn test_finalize_binary_unix_permissions() {
use std::os::unix::fs::PermissionsExt;
let temp = TempDir::new().unwrap();
let project_dir = temp.path().join("project");
let target_release = project_dir.join("target/release");
fs::create_dir_all(&target_release).unwrap();
fs::write(target_release.join("perm_test"), "binary content").unwrap();
let input = temp.path().join("perm_test.py");
fs::write(&input, "").unwrap();
let output_path = finalize_binary(&project_dir, &input, None, "release").unwrap();
let perms = fs::metadata(&output_path).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o755);
}
#[test]
fn test_extract_e0308_constraints_empty_stderr() {
let source = Path::new("test.py");
let store = extract_e0308_constraints("", source);
assert_eq!(store.stats.constraints_extracted, 0);
}
#[test]
fn test_extract_e0308_constraints_only_warnings() {
let source = Path::new("test.py");
let stderr = "warning: unused variable `x`\nwarning: dead code\n";
let store = extract_e0308_constraints(stderr, source);
assert_eq!(store.stats.constraints_extracted, 0);
}
#[test]
fn test_build_result_fields() {
let temp = TempDir::new().unwrap();
let project_dir = temp.path().to_path_buf();
let src_dir = project_dir.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("main.rs"), "fn main() { let x: i32 = \"oops\"; }").unwrap();
let cargo_toml = "[package]\nname = \"test_fields\"\nversion = \"0.1.0\"\nedition = \"2021\"\n";
fs::write(project_dir.join("Cargo.toml"), cargo_toml).unwrap();
let result = build_cargo_project(&project_dir, "release").unwrap();
assert!(!result.success);
assert!(!result.stderr.is_empty());
}
#[test]
fn test_create_cargo_project_sanitizes_name() {
let rust_code = r#"pub fn greet() -> &'static str { "hello" }"#;
let temp = TempDir::new().unwrap();
let input = temp.path().join("my-cool-lib.py");
fs::write(&input, "").unwrap();
let dependencies = vec![];
let (project_dir, _) = create_cargo_project(&input, rust_code, &dependencies).unwrap();
let cargo_content = fs::read_to_string(project_dir.join("Cargo.toml")).unwrap();
assert!(cargo_content.contains("my_cool_lib") || cargo_content.contains("my-cool-lib"));
}
#[test]
fn test_compile_nonexistent_error_message() {
let result = compile_python_to_binary(
Path::new("/absolutely/nonexistent/file.py"),
None,
Some("release"),
);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not found"));
assert!(err_msg.contains("nonexistent"));
}
#[test]
fn test_compile_with_none_profile_defaults_release() {
let result = compile_python_to_binary(
Path::new("/nonexistent/file.py"),
None,
None, );
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
}