use anyhow::Result;
use clap::{Parser, Subcommand};
use colored::Colorize;
use depyler_analyzer::Analyzer;
use depyler_core::DepylerPipeline;
use std::fs;
use std::path::PathBuf;
pub mod cli_shim;
pub mod compile_cmd;
pub mod converge;
pub mod dashboard_cmd;
pub mod graph_cmd;
pub mod lint_cmd;
pub mod report_cmd;
pub mod report_shim;
pub mod score_cmd;
pub mod transpile_shim;
pub mod utol_cmd;
pub mod prelude;
pub mod python_ops;
#[derive(Parser)]
#[command(name = "depyler")]
#[command(about = "Pragmatic Python-to-Rust Transpiler", version)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(short, long, global = true)]
pub verbose: bool,
}
#[derive(Subcommand)]
pub enum Commands {
Transpile {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
verify: bool,
#[arg(long)]
gen_tests: bool,
#[arg(long)]
debug: bool,
#[arg(long)]
source_map: bool,
},
Compile {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long, default_value = "release")]
profile: String,
},
Analyze {
input: PathBuf,
#[arg(short, long, default_value = "text")]
format: String,
},
Check {
input: PathBuf,
},
Converge {
#[arg(long)]
input_dir: PathBuf,
#[arg(long, default_value = "100")]
target_rate: f64,
#[arg(long, default_value = "50")]
max_iterations: usize,
#[arg(long)]
auto_fix: bool,
#[arg(long)]
dry_run: bool,
#[arg(long, default_value = "0.8")]
fix_confidence: f64,
#[arg(long)]
checkpoint: Option<PathBuf>,
#[arg(long, default_value = "4")]
jobs: usize,
#[arg(long, default_value = "rich")]
display: String,
#[arg(long)]
oracle: bool,
#[arg(long)]
explain: bool,
#[arg(long, default_value = "true")]
cache: bool,
#[arg(long)]
patch_transpiler: bool,
#[arg(long)]
apr_file: Option<PathBuf>,
},
Train {
#[arg(long)]
corpus: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long, default_value = "80")]
target_rate: f64,
#[arg(long, default_value = "10")]
max_iterations: usize,
},
Report {
#[arg(long)]
input_dir: PathBuf,
#[arg(short, long, default_value = "text")]
format: String,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
filter_error: Option<String>,
#[arg(long)]
filter_file: Option<String>,
#[arg(long)]
failures_only: bool,
#[arg(long)]
verbose: bool,
},
Utol {
#[arg(long)]
corpus: Option<PathBuf>,
#[arg(long, default_value = "0.80")]
target_rate: f64,
#[arg(long, default_value = "50")]
max_iterations: usize,
#[arg(long, default_value = "5")]
patience: usize,
#[arg(long, default_value = "rich")]
display: String,
#[arg(long)]
status: bool,
},
#[command(subcommand)]
Cache(CacheCommands),
Repair {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long, default_value = "10")]
max_iterations: usize,
#[arg(short, long)]
verbose: bool,
},
#[command(subcommand)]
Graph(GraphCommands),
#[command(subcommand)]
Corpus(CorpusCommands),
Lint {
input: PathBuf,
#[arg(long)]
strict: bool,
#[arg(short, long, default_value = "text")]
format: String,
#[arg(long)]
fail_fast: bool,
#[arg(long)]
corpus: bool,
},
Dashboard {
#[arg(short, long, default_value = "text")]
format: String,
#[arg(long)]
component: Option<String>,
},
}
#[derive(Subcommand)]
pub enum GraphCommands {
Analyze {
#[arg(long)]
corpus: PathBuf,
#[arg(long, default_value = "5")]
top: usize,
#[arg(short, long)]
output: Option<PathBuf>,
},
Vectorize {
#[arg(long)]
corpus: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long, default_value = "ndjson")]
format: String,
},
}
#[derive(Subcommand)]
pub enum CacheCommands {
Stats {
#[arg(short, long, default_value = "text")]
format: String,
},
Gc {
#[arg(long, default_value = "30")]
max_age_days: u32,
#[arg(long)]
dry_run: bool,
},
Clear {
#[arg(long)]
force: bool,
},
Warm {
#[arg(long)]
input_dir: PathBuf,
#[arg(long, default_value = "4")]
jobs: usize,
},
}
#[derive(Subcommand)]
pub enum CorpusCommands {
List {
#[arg(short, long, default_value = "text")]
format: String,
#[arg(long)]
available: bool,
},
Show {
name: String,
},
Add {
#[arg(long)]
name: String,
#[arg(long)]
path: PathBuf,
#[arg(long)]
description: Option<String>,
#[arg(long)]
github: Option<String>,
},
}
pub fn transpile_command(
input: PathBuf,
output: Option<PathBuf>,
verify: bool,
gen_tests: bool,
debug: bool,
source_map: bool,
) -> Result<()> {
use depyler_core::cargo_toml_gen::{generate_cargo_toml_auto, Dependency};
let python_source = fs::read_to_string(&input)?;
let pipeline = DepylerPipeline::new();
let (rust_code, dependencies) = match pipeline.transpile_with_dependencies(&python_source) {
Ok(result) => result,
Err(err) => {
let diagnostic = depyler_core::diagnostic::Diagnostic::from_anyhow(
&err,
Some(input.display().to_string()),
Some(&python_source),
);
eprintln!("{}", diagnostic);
anyhow::bail!("transpilation failed");
}
};
let output_path = output.unwrap_or_else(|| input.with_extension("rs"));
fs::write(&output_path, &rust_code)?;
println!("{} {}", "✓".green(), output_path.display());
{
let package_name = output_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("transpiled")
.replace('-', "_");
let source_file_name = output_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("main.rs");
let deps: Vec<Dependency> = dependencies;
let cargo_toml = generate_cargo_toml_auto(&package_name, source_file_name, &deps);
let cargo_toml_path = output_path
.parent()
.unwrap_or(std::path::Path::new("."))
.join("Cargo.toml");
fs::write(&cargo_toml_path, cargo_toml)?;
println!(
"{} {} (with {} dependencies)",
"✓".green(),
cargo_toml_path.display(),
deps.len()
);
}
if verify {
println!("Verification not yet implemented");
}
if gen_tests {
println!("Test generation not yet implemented");
}
if debug {
println!("Debug mode enabled");
}
if source_map {
println!("Source map generation not yet implemented");
}
Ok(())
}
pub fn compile_command(input: PathBuf, output: Option<PathBuf>, profile: String) -> Result<()> {
let output_ref = output.as_deref();
let profile_ref = if profile.is_empty() {
None
} else {
Some(profile.as_str())
};
compile_cmd::compile_python_to_binary(&input, output_ref, profile_ref)?;
Ok(())
}
pub fn analyze_command(input: PathBuf, format: String) -> Result<()> {
let python_source = fs::read_to_string(&input)?;
let pipeline = DepylerPipeline::new();
let hir = pipeline.parse_to_hir(&python_source)?;
let analyzer = Analyzer::new();
let report = analyzer.analyze(&hir)?;
match format.as_str() {
"json" => println!("{}", serde_json::to_string_pretty(&report)?),
_ => println!("{:#?}", report),
}
Ok(())
}
pub fn check_command(input: PathBuf) -> Result<()> {
let python_source = fs::read_to_string(&input)?;
let pipeline = DepylerPipeline::new();
match pipeline.transpile(&python_source) {
Ok(_) => {
println!("{} {} can be transpiled", "✓".green(), input.display());
Ok(())
}
Err(e) => {
let diagnostic = depyler_core::diagnostic::Diagnostic::from_anyhow(
&e,
Some(input.display().to_string()),
Some(&python_source),
);
eprintln!("{}", diagnostic);
anyhow::bail!("check failed")
}
}
}
pub fn repair_command(
input: PathBuf,
output: Option<PathBuf>,
max_iterations: usize,
verbose: bool,
) -> Result<()> {
use depyler_oracle::utol::repair_file_types;
if verbose {
println!(
"{} Starting type repair for {}",
"🔧".green(),
input.display()
);
println!(" Max iterations: {}", max_iterations);
}
let result = repair_file_types(&input, max_iterations)?;
if result.success {
println!(
"{} {} repaired successfully in {} iterations",
"✓".green(),
input.display(),
result.iterations
);
println!(
" Constraints learned: {}, applied: {}",
result.constraints_learned, result.constraints_applied
);
let pipeline = DepylerPipeline::new();
let python_source = fs::read_to_string(&input)?;
let rust_code = pipeline.transpile(&python_source)?;
let output_path = output.unwrap_or_else(|| input.with_extension("rs"));
fs::write(&output_path, &rust_code)?;
println!(
"{} Wrote repaired Rust code to {}",
"📄".green(),
output_path.display()
);
Ok(())
} else {
println!(
"{} {} repair failed after {} iterations",
"✗".red(),
input.display(),
result.iterations
);
println!(
" Constraints learned: {}, applied: {}",
result.constraints_learned, result.constraints_applied
);
println!(" Final compile rate: {:.1}%", result.final_rate * 100.0);
anyhow::bail!(
"Type repair failed after {} iterations. Consider manual fixes.",
max_iterations
)
}
}
pub fn complexity_rating(complexity: f64) -> colored::ColoredString {
if complexity < 10.0 {
"Low".green()
} else if complexity < 20.0 {
"Medium".yellow()
} else {
"High".red()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_complexity_rating_low() {
let rating = complexity_rating(5.0);
assert!(!rating.to_string().is_empty());
}
#[test]
fn test_complexity_rating_medium() {
let rating = complexity_rating(15.0);
assert!(!rating.to_string().is_empty());
}
#[test]
fn test_complexity_rating_high() {
let rating = complexity_rating(25.0);
assert!(!rating.to_string().is_empty());
}
#[test]
fn test_complexity_rating_boundary_low() {
let rating = complexity_rating(9.99);
assert!(rating.to_string().contains("Low"));
}
#[test]
fn test_complexity_rating_boundary_medium() {
let rating = complexity_rating(10.0);
assert!(rating.to_string().contains("Medium"));
}
#[test]
fn test_complexity_rating_boundary_high() {
let rating = complexity_rating(20.0);
assert!(rating.to_string().contains("High"));
}
#[test]
fn test_complexity_rating_zero() {
let rating = complexity_rating(0.0);
assert!(rating.to_string().contains("Low"));
}
#[test]
fn test_complexity_rating_negative() {
let rating = complexity_rating(-5.0);
assert!(rating.to_string().contains("Low"));
}
#[test]
fn test_transpile_command_valid() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("test.py");
fs::write(
&py_file,
"def add(a: int, b: int) -> int:\n return a + b\n",
)
.unwrap();
let result = transpile_command(py_file.clone(), None, false, false, false, false);
assert!(result.is_ok());
let rs_file = py_file.with_extension("rs");
assert!(rs_file.exists());
}
#[test]
fn test_transpile_command_with_output() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("test.py");
let rs_file = temp.path().join("custom_output.rs");
fs::write(&py_file, "def greet() -> str:\n return 'hello'\n").unwrap();
let result = transpile_command(py_file, Some(rs_file.clone()), false, false, false, false);
assert!(result.is_ok());
assert!(rs_file.exists());
}
#[test]
fn test_transpile_command_nonexistent() {
let result = transpile_command(
PathBuf::from("/nonexistent.py"),
None,
false,
false,
false,
false,
);
assert!(result.is_err());
}
#[test]
fn test_transpile_command_with_verify() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("test.py");
fs::write(&py_file, "x = 1\n").unwrap();
let result = transpile_command(py_file, None, true, false, false, false);
assert!(result.is_ok());
}
#[test]
fn test_transpile_command_with_gen_tests() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("test.py");
fs::write(&py_file, "x = 1\n").unwrap();
let result = transpile_command(py_file, None, false, true, false, false);
assert!(result.is_ok());
}
#[test]
fn test_transpile_command_with_debug() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("test.py");
fs::write(&py_file, "x = 1\n").unwrap();
let result = transpile_command(py_file, None, false, false, true, false);
assert!(result.is_ok());
}
#[test]
fn test_transpile_command_with_source_map() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("test.py");
fs::write(&py_file, "x = 1\n").unwrap();
let result = transpile_command(py_file, None, false, false, false, true);
assert!(result.is_ok());
}
#[test]
fn test_transpile_command_all_flags() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("test.py");
fs::write(&py_file, "def foo(): pass\n").unwrap();
let result = transpile_command(py_file, None, true, true, true, true);
assert!(result.is_ok());
}
#[test]
fn test_analyze_command_text_format() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("analyze.py");
fs::write(
&py_file,
"def add(a: int, b: int) -> int:\n return a + b\n",
)
.unwrap();
let result = analyze_command(py_file, "text".to_string());
assert!(result.is_ok());
}
#[test]
fn test_analyze_command_json_format() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("analyze.py");
fs::write(
&py_file,
"def add(a: int, b: int) -> int:\n return a + b\n",
)
.unwrap();
let result = analyze_command(py_file, "json".to_string());
assert!(result.is_ok());
}
#[test]
fn test_analyze_command_nonexistent() {
let result = analyze_command(PathBuf::from("/nonexistent.py"), "text".to_string());
assert!(result.is_err());
}
#[test]
fn test_check_command_valid() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("check.py");
fs::write(
&py_file,
"def add(a: int, b: int) -> int:\n return a + b\n",
)
.unwrap();
let result = check_command(py_file);
assert!(result.is_ok());
}
#[test]
fn test_check_command_nonexistent() {
let result = check_command(PathBuf::from("/nonexistent.py"));
assert!(result.is_err());
}
#[test]
fn test_compile_command_nonexistent() {
let result = compile_command(
PathBuf::from("/nonexistent.py"),
None,
"release".to_string(),
);
assert!(result.is_err());
}
#[test]
fn test_compile_command_empty_profile() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("compile.py");
fs::write(&py_file, "def foo(): pass\n").unwrap();
let result = compile_command(py_file, None, "".to_string());
let _ = result;
}
#[test]
fn test_compile_command_with_profile() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("compile.py");
fs::write(&py_file, "def foo(): pass\n").unwrap();
let result = compile_command(py_file, None, "debug".to_string());
let _ = result;
}
#[test]
fn test_cli_verbose_default() {
use clap::Parser;
let cli = Cli::try_parse_from(["depyler", "check", "test.py"]).unwrap();
assert!(!cli.verbose);
}
#[test]
fn test_cache_commands_stats() {
use clap::Parser;
let cli = Cli::try_parse_from(["depyler", "cache", "stats"]).unwrap();
if let Commands::Cache(CacheCommands::Stats { format }) = cli.command {
assert_eq!(format, "text");
} else {
panic!("Expected Cache Stats");
}
}
#[test]
fn test_cache_commands_gc() {
use clap::Parser;
let cli = Cli::try_parse_from(["depyler", "cache", "gc"]).unwrap();
if let Commands::Cache(CacheCommands::Gc {
max_age_days,
dry_run,
}) = cli.command
{
assert_eq!(max_age_days, 30);
assert!(!dry_run);
} else {
panic!("Expected Cache Gc");
}
}
#[test]
fn test_cache_commands_clear() {
use clap::Parser;
let cli = Cli::try_parse_from(["depyler", "cache", "clear"]).unwrap();
if let Commands::Cache(CacheCommands::Clear { force }) = cli.command {
assert!(!force);
} else {
panic!("Expected Cache Clear");
}
}
#[test]
fn test_cache_commands_warm() {
use clap::Parser;
let cli = Cli::try_parse_from(["depyler", "cache", "warm", "--input-dir", "/tmp"]).unwrap();
if let Commands::Cache(CacheCommands::Warm { input_dir, jobs }) = cli.command {
assert_eq!(input_dir, PathBuf::from("/tmp"));
assert_eq!(jobs, 4);
} else {
panic!("Expected Cache Warm");
}
}
#[test]
fn test_complexity_rating_infinity() {
let rating = complexity_rating(f64::INFINITY);
assert!(rating.to_string().contains("High"));
}
#[test]
fn test_complexity_rating_large() {
let rating = complexity_rating(1e10);
assert!(rating.to_string().contains("High"));
}
#[test]
fn test_complexity_rating_nan() {
let rating = complexity_rating(f64::NAN);
assert!(rating.to_string().contains("High"));
}
#[test]
fn test_transpile_command_with_cargo_toml() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("lib_test.py");
fs::write(
&py_file,
"def add(a: int, b: int) -> int:\n return a + b\n",
)
.unwrap();
let output = temp.path().join("lib_test.rs");
let result = transpile_command(py_file, Some(output.clone()), false, false, false, false);
assert!(result.is_ok());
assert!(output.exists());
let cargo_toml = temp.path().join("Cargo.toml");
assert!(cargo_toml.exists());
}
#[test]
fn test_check_command_invalid_syntax() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("bad_syntax.py");
fs::write(&py_file, "def @@@invalid syntax!!!").unwrap();
let result = check_command(py_file);
assert!(result.is_err());
}
#[test]
fn test_analyze_command_unknown_format() {
let temp = TempDir::new().unwrap();
let py_file = temp.path().join("analyze.py");
fs::write(
&py_file,
"def add(a: int, b: int) -> int:\n return a + b\n",
)
.unwrap();
let result = analyze_command(py_file, "xml".to_string());
assert!(result.is_ok());
}
}