use anyhow::{Context, Result};
use ruchy::frontend::ast::Expr;
use ruchy::{Parser as RuchyParser, Transpiler};
use std::fs;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
pub fn handle_transpile_command(
file: &Path,
output: Option<&Path>,
minimal: bool,
verbose: bool,
) -> Result<()> {
log_transpile_start(file, minimal, verbose);
let source = read_source_file(file, verbose)?;
let ast = parse_source(&source)?;
let rust_code = transpile_ast(&ast, minimal)?;
write_output(&rust_code, output, verbose)?;
Ok(())
}
#[allow(dead_code)]
pub fn derive_default_output_path(file: &Path) -> Option<PathBuf> {
if file.as_os_str() == "-" {
return None; }
let stem = file.file_stem()?;
let parent = file.parent().unwrap_or(Path::new("."));
Some(parent.join(format!("{}.rs", stem.to_string_lossy())))
}
fn log_transpile_start(file: &Path, minimal: bool, verbose: bool) {
if !verbose {
return;
}
eprintln!("Transpiling file: {}", file.display());
if minimal {
eprintln!("Using minimal codegen for self-hosting");
}
}
pub fn read_source_file(file: &Path, verbose: bool) -> Result<String> {
if file.as_os_str() == "-" {
if verbose {
eprintln!("Reading from stdin...");
}
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
Ok(input)
} else {
fs::read_to_string(file).with_context(|| format!("Failed to read file: {}", file.display()))
}
}
pub fn parse_source(source: &str) -> Result<Expr> {
let mut parser = RuchyParser::new(source);
parser.parse().with_context(|| "Failed to parse input")
}
pub fn transpile_ast(ast: &Expr, minimal: bool) -> Result<String> {
let mut transpiler = Transpiler::new();
if minimal {
transpiler
.transpile_minimal(ast)
.with_context(|| "Failed to transpile to Rust (minimal)")
} else {
let tokens = transpiler
.transpile_to_program(ast)
.with_context(|| "Failed to transpile to Rust")?;
let syntax_tree = syn::parse2(tokens)
.with_context(|| "Failed to parse generated tokens as Rust syntax")?;
Ok(prettyplease::unparse(&syntax_tree))
}
}
pub fn write_output(rust_code: &str, output: Option<&Path>, verbose: bool) -> Result<()> {
if let Some(output_path) = output {
if output_path.as_os_str() == "-" {
print!("{rust_code}");
} else {
super::write_file_with_context(output_path, rust_code.as_bytes())?;
if verbose {
eprintln!("Output written to: {}", output_path.display());
}
}
} else {
print!("{rust_code}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_derive_default_output_path_ruchy() {
let path = Path::new("src/main.ruchy");
let output = derive_default_output_path(path);
assert_eq!(output, Some(PathBuf::from("src/main.rs")));
}
#[test]
fn test_derive_default_output_path_no_extension() {
let path = Path::new("script");
let output = derive_default_output_path(path);
assert_eq!(output, Some(PathBuf::from("script.rs")));
}
#[test]
fn test_derive_default_output_path_stdin() {
let path = Path::new("-");
let output = derive_default_output_path(path);
assert_eq!(output, None);
}
#[test]
fn test_parse_source_simple() {
let source = "42";
let ast = parse_source(source);
assert!(ast.is_ok());
}
#[test]
fn test_parse_source_invalid() {
let source = "let = invalid";
let ast = parse_source(source);
assert!(ast.is_err());
}
#[test]
fn test_transpile_ast_simple() {
let source = "fun main() { 42 }";
let ast = parse_source(source).unwrap();
let rust_code = transpile_ast(&ast, false);
assert!(rust_code.is_ok());
}
#[test]
fn test_transpile_ast_minimal() {
let source = "42";
let ast = parse_source(source).unwrap();
let rust_code = transpile_ast(&ast, true);
assert!(rust_code.is_ok());
}
#[test]
fn test_log_transpile_start_verbose() {
let file = Path::new("test.ruchy");
log_transpile_start(file, true, true);
log_transpile_start(file, false, true);
log_transpile_start(file, true, false);
}
#[test]
fn test_derive_default_output_path_nested() {
let path = Path::new("/path/to/nested/script.ruchy");
let output = derive_default_output_path(path);
assert_eq!(output, Some(PathBuf::from("/path/to/nested/script.rs")));
}
#[test]
fn test_derive_default_output_path_current_dir() {
let path = Path::new("./script.ruchy");
let output = derive_default_output_path(path);
assert_eq!(output, Some(PathBuf::from("./script.rs")));
}
#[test]
fn test_read_source_file_nonexistent() {
let path = Path::new("/nonexistent/file.ruchy");
let result = read_source_file(path, false);
assert!(result.is_err());
}
#[test]
fn test_read_source_file_verbose_nonexistent() {
let path = Path::new("/nonexistent/file.ruchy");
let result = read_source_file(path, true);
assert!(result.is_err());
}
#[test]
fn test_parse_source_empty() {
let source = "";
let result = parse_source(source);
assert!(result.is_ok()); }
#[test]
fn test_parse_source_function() {
let source = "fun add(a, b) { a + b }";
let result = parse_source(source);
assert!(result.is_ok());
}
#[test]
fn test_transpile_ast_with_function() {
let source = "fun greet(name) { println(name) }";
let ast = parse_source(source).unwrap();
let rust_code = transpile_ast(&ast, false);
assert!(rust_code.is_ok());
let code = rust_code.unwrap();
assert!(code.contains("fn greet"));
}
#[test]
fn test_handle_transpile_command_nonexistent() {
let path = Path::new("/nonexistent/file.ruchy");
let result = handle_transpile_command(path, None, false, false);
assert!(result.is_err());
}
#[test]
fn test_handle_transpile_command_verbose_nonexistent() {
let path = Path::new("/nonexistent/file.ruchy");
let result = handle_transpile_command(path, None, false, true);
assert!(result.is_err());
}
#[test]
fn test_write_output_to_stdout() {
let rust_code = "fn main() {}";
let result = write_output(rust_code, None, false);
assert!(result.is_ok());
}
#[test]
fn test_write_output_to_explicit_stdout() {
let rust_code = "fn main() {}";
let result = write_output(rust_code, Some(Path::new("-")), false);
assert!(result.is_ok());
}
#[test]
fn test_log_transpile_start_all_combinations() {
let file = Path::new("test.ruchy");
log_transpile_start(file, false, false);
log_transpile_start(file, true, false);
log_transpile_start(file, false, true);
log_transpile_start(file, true, true);
}
}