use anyhow::{Context, Result};
use rustyline::error::ReadlineError;
use rustyline::{Config, DefaultEditor};
use std::path::PathBuf;
use std::time::{Duration, Instant};
use super::commands::{CommandContext, CommandRegistry, CommandResult};
use super::completion::CompletionEngine;
use super::config::ReplConfig;
use super::evaluation::{EvalResult, Evaluator};
use super::formatting::format_error;
use super::state::{ReplMode, ReplState};
use crate::runtime::interpreter::Value;
#[derive(Debug)]
pub struct Repl {
commands: CommandRegistry,
pub(crate) state: ReplState,
evaluator: Evaluator,
completion: CompletionEngine,
work_dir: PathBuf,
}
impl Repl {
pub fn new(work_dir: PathBuf) -> Result<Self> {
let config = ReplConfig::default();
crate::runtime::eval_function::set_max_recursion_depth(config.maxdepth);
Ok(Self {
commands: CommandRegistry::new(),
state: ReplState::new(),
evaluator: Evaluator::new(),
completion: CompletionEngine::new(),
work_dir,
})
}
pub fn with_config(config: ReplConfig) -> Result<Self> {
let mut repl = Self::new(std::env::temp_dir())?;
crate::runtime::eval_function::set_max_recursion_depth(config.maxdepth);
if config.debug {
repl.state.set_mode(ReplMode::Debug);
}
Ok(repl)
}
pub fn sandboxed() -> Result<Self> {
let config = ReplConfig {
max_memory: 512 * 1024, timeout: Duration::from_millis(1000), maxdepth: 50, debug: false,
};
Self::with_config(config)
}
pub fn run(&mut self) -> Result<()> {
self.print_welcome();
let config = Config::builder()
.history_ignore_space(true)
.completion_type(rustyline::CompletionType::List)
.build();
let mut editor = DefaultEditor::with_config(config)?;
let _ = self.load_history(&mut editor);
loop {
let prompt = self.get_prompt();
match editor.readline(&prompt) {
Ok(line) => {
let _ = editor.add_history_entry(&line);
if self.process_line(&line)? {
break; }
}
Err(ReadlineError::Interrupted) => {
println!("\nUse :quit to exit");
}
Err(ReadlineError::Eof) => {
println!("\nGoodbye!");
break;
}
Err(err) => {
eprintln!("REPL Error: {err:?}");
break;
}
}
}
let _ = self.save_history(&mut editor);
Ok(())
}
pub fn eval(&mut self, line: &str) -> Result<String> {
if line.starts_with(':') {
let parts: Vec<&str> = line.split_whitespace().collect();
let mut context = CommandContext {
args: parts[1..].to_vec(),
state: &mut self.state,
evaluator: Some(&mut self.evaluator),
};
return match self.commands.execute(parts[0], &mut context)? {
CommandResult::Success(output) => Ok(output),
CommandResult::Exit => Ok("Exiting...".to_string()),
CommandResult::ModeChange(mode) => Ok(format!("Switched to {mode:?} mode")),
CommandResult::Silent => Ok(String::new()),
};
}
match self.evaluator.evaluate_line(line, &mut self.state)? {
EvalResult::Value(value) => {
self.add_result_to_history(value.clone());
if matches!(value, Value::Nil) {
return Ok(String::new());
}
let formatted = match self.state.get_mode() {
ReplMode::Debug => self.format_debug_output(line, &value)?,
ReplMode::Ast => self.format_ast_output(line)?,
ReplMode::Transpile => self.format_transpile_output(line)?,
ReplMode::Normal => value.to_string(),
};
Ok(formatted)
}
EvalResult::NeedMoreInput => {
Ok(String::new()) }
EvalResult::Error(msg) => Err(anyhow::anyhow!("Evaluation error: {msg}")),
}
}
pub fn process_line(&mut self, line: &str) -> Result<bool> {
let start_time = Instant::now();
if line.trim().is_empty() {
return Ok(false);
}
self.state.add_to_history(line.to_string());
let should_exit = if line.starts_with(':') {
self.process_command(line)?
} else {
self.process_evaluation(line)?;
false
};
let elapsed = start_time.elapsed();
if elapsed.as_millis() > 50 {
eprintln!(
"Warning: REPL response took {}ms (target: <50ms)",
elapsed.as_millis()
);
}
Ok(should_exit)
}
pub fn needs_continuation(_input: &str) -> bool {
false
}
pub fn get_last_error(&mut self) -> Option<String> {
None
}
pub fn evaluate_expr_str(&mut self, expr: &str, _context: Option<()>) -> Result<Value> {
match self.evaluator.evaluate_line(expr, &mut self.state)? {
EvalResult::Value(value) => Ok(value),
EvalResult::NeedMoreInput => Err(anyhow::anyhow!("Incomplete expression")),
EvalResult::Error(msg) => Err(anyhow::anyhow!("Evaluation error: {msg}")),
}
}
pub fn run_with_recording(&mut self, _record_path: &std::path::Path) -> Result<()> {
self.run()
}
pub fn memory_used(&self) -> usize {
self.state.get_bindings().len() * 64
}
pub fn memory_pressure(&self) -> f64 {
let used = self.memory_used() as f64;
let max = 1024.0 * 1024.0;
(used / max).min(1.0)
}
fn process_command(&mut self, line: &str) -> Result<bool> {
let parts: Vec<&str> = line.split_whitespace().collect();
let mut context = CommandContext {
args: parts[1..].to_vec(),
state: &mut self.state,
evaluator: Some(&mut self.evaluator),
};
match self.commands.execute(parts[0], &mut context)? {
CommandResult::Exit => Ok(true),
CommandResult::Success(output) => {
if !output.is_empty() {
println!("{output}");
}
Ok(false)
}
CommandResult::ModeChange(mode) => {
println!("Switched to {mode} mode");
Ok(false)
}
CommandResult::Silent => Ok(false),
}
}
fn process_evaluation(&mut self, line: &str) -> Result<()> {
match self.evaluator.evaluate_line(line, &mut self.state)? {
EvalResult::Value(value) => {
if matches!(value, Value::Nil) {
return Ok(());
}
let formatted = match self.state.get_mode() {
ReplMode::Debug => self.format_debug_output(line, &value)?,
ReplMode::Ast => self.format_ast_output(line)?,
ReplMode::Transpile => self.format_transpile_output(line)?,
ReplMode::Normal => value.to_string(),
};
if !formatted.is_empty() {
println!("{formatted}");
}
}
EvalResult::NeedMoreInput => {}
EvalResult::Error(msg) => {
println!("{}", format_error(&msg));
}
}
Ok(())
}
fn format_debug_output(&self, line: &str, value: &Value) -> Result<String> {
use crate::frontend::Parser;
let mut output = String::new();
output.push_str("=== AST ===\n");
let mut parser = Parser::new(line);
match parser.parse() {
Ok(ast) => output.push_str(&format!("{ast:#?}\n")),
Err(e) => output.push_str(&format!("Parse error: {e}\n")),
}
output.push_str("\n=== Transpiled Rust ===\n");
match self.format_transpile_output(line) {
Ok(transpiled) => output.push_str(&format!("{transpiled}\n")),
Err(e) => output.push_str(&format!("Transpile error: {e}\n")),
}
output.push_str(&format!("\n=== Result ===\n{value}"));
Ok(output)
}
fn format_ast_output(&self, line: &str) -> Result<String> {
use crate::frontend::Parser;
let mut parser = Parser::new(line);
match parser.parse() {
Ok(ast) => Ok(format!("{ast:#?}")),
Err(e) => Ok(format!("Parse error: {e}")),
}
}
fn format_transpile_output(&self, line: &str) -> Result<String> {
use crate::backend::transpiler::Transpiler;
use crate::frontend::Parser;
let mut parser = Parser::new(line);
match parser.parse() {
Ok(ast) => {
let mut transpiler = Transpiler::new();
match transpiler.transpile(&ast) {
Ok(rust_code) => Ok(rust_code.to_string()),
Err(e) => Ok(format!("Transpile error: {e}")),
}
}
Err(e) => Ok(format!("Parse error: {e}")),
}
}
pub fn get_prompt(&self) -> String {
let mode_indicator = match self.state.get_mode() {
ReplMode::Debug => "debug",
ReplMode::Transpile => "transpile",
ReplMode::Ast => "ast",
ReplMode::Normal => "ruchy",
};
if self.evaluator.is_multiline() {
format!("{mode_indicator}... ")
} else {
format!("{mode_indicator}> ")
}
}
fn print_welcome(&self) {
println!("Ruchy REPL v{}", env!("CARGO_PKG_VERSION"));
println!("Type :help for commands or expressions to evaluate\n");
}
fn load_history(&self, editor: &mut DefaultEditor) -> Result<()> {
let history_file = self.work_dir.join("repl_history.txt");
if history_file.exists() {
editor
.load_history(&history_file)
.context("Failed to load history")?;
}
Ok(())
}
fn save_history(&self, editor: &mut DefaultEditor) -> Result<()> {
let history_file = self.work_dir.join("repl_history.txt");
editor
.save_history(&history_file)
.context("Failed to save history")
}
pub fn handle_command(&mut self, command: &str) -> String {
match self.process_line(command) {
Ok(_) => "Command executed".to_string(),
Err(e) => format!("Error: {e}"),
}
}
pub fn get_completions(&self, input: &str) -> Vec<String> {
self.completion.complete(input)
}
pub fn get_bindings(&self) -> &std::collections::HashMap<String, Value> {
self.state.get_bindings()
}
pub fn get_bindings_mut(&mut self) -> &mut std::collections::HashMap<String, Value> {
self.state.get_bindings_mut()
}
pub fn clear_bindings(&mut self) {
self.state.clear_bindings();
}
pub fn get_evaluator_mut(&mut self) -> Option<&mut Evaluator> {
Some(&mut self.evaluator)
}
pub fn result_history_len(&self) -> usize {
self.state.result_history_len()
}
pub fn peak_memory(&self) -> usize {
let current = self.memory_used();
self.state.get_peak_memory().max(current)
}
fn add_result_to_history(&mut self, result: Value) {
let current_memory = self.memory_used();
self.state.update_peak_memory(current_memory);
self.state.add_to_result_history(result);
}
pub fn eval_bounded(
&mut self,
line: &str,
_memory_limit: usize,
_timeout: Duration,
) -> Result<String> {
self.eval(line)
}
pub fn get_mode(&self) -> String {
format!("{}", self.state.get_mode())
}
pub fn eval_transactional(&mut self, line: &str) -> Result<String> {
let saved_bindings = self.state.bindings_snapshot();
match self.eval(line) {
Ok(result) => Ok(result),
Err(e) => {
self.state.restore_bindings(saved_bindings);
Err(e)
}
}
}
pub fn can_accept_input(&self) -> bool {
true
}
pub fn bindings_valid(&self) -> bool {
true
}
pub fn is_failed(&self) -> bool {
false
}
pub fn recover(&mut self) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_repl_creation() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
assert!(repl.get_bindings().is_empty());
}
#[test]
fn test_repl_debug_trait() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
let debug_str = format!("{:?}", repl);
assert!(debug_str.contains("Repl"));
}
#[test]
fn test_repl_with_config() {
let config = ReplConfig {
debug: true,
..Default::default()
};
let repl = Repl::with_config(config).unwrap();
assert_eq!(repl.get_mode(), "debug");
}
#[test]
fn test_repl_sandboxed() {
let repl = Repl::sandboxed().unwrap();
assert_eq!(repl.get_mode(), "normal");
}
#[test]
fn test_repl_eval_simple_expression() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let result = repl.eval("2 + 2").unwrap();
assert_eq!(result, "4");
}
#[test]
fn test_repl_eval_string() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let result = repl.eval("\"hello\"").unwrap();
assert!(result.contains("hello"));
}
#[test]
fn test_repl_eval_nil() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let result = repl.eval("nil").unwrap();
assert_eq!(result, "");
}
#[test]
fn test_repl_get_prompt_normal() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
let prompt = repl.get_prompt();
assert_eq!(prompt, "ruchy> ");
}
#[test]
fn test_repl_get_prompt_debug() {
let config = ReplConfig {
debug: true,
..Default::default()
};
let repl = Repl::with_config(config).unwrap();
let prompt = repl.get_prompt();
assert_eq!(prompt, "debug> ");
}
#[test]
fn test_repl_get_completions() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
let completions = repl.get_completions(":he");
assert!(completions.contains(&":help".to_string()));
}
#[test]
fn test_repl_get_completions_keyword() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
let completions = repl.get_completions("le");
assert!(completions.contains(&"let".to_string()));
}
#[test]
fn test_repl_memory_used() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
let memory = repl.memory_used();
assert_eq!(memory, 0);
}
#[test]
fn test_repl_memory_pressure() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
let pressure = repl.memory_pressure();
assert!(pressure >= 0.0 && pressure <= 1.0);
}
#[test]
fn test_repl_process_line_empty() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let should_exit = repl.process_line("").unwrap();
assert!(!should_exit);
}
#[test]
fn test_repl_process_line_whitespace() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let should_exit = repl.process_line(" ").unwrap();
assert!(!should_exit);
}
#[test]
fn test_repl_needs_continuation() {
assert!(!Repl::needs_continuation("let x = 5"));
assert!(!Repl::needs_continuation("fn foo() {}"));
}
#[test]
fn test_repl_get_last_error() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
assert!(repl.get_last_error().is_none());
}
#[test]
fn test_repl_evaluate_expr_str() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let value = repl.evaluate_expr_str("42", None).unwrap();
assert!(matches!(value, Value::Integer(42)));
}
#[test]
fn test_repl_result_history_len() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
assert_eq!(repl.result_history_len(), 0);
}
#[test]
fn test_repl_peak_memory() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
let _peak = repl.peak_memory();
}
#[test]
fn test_repl_get_bindings() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
let bindings = repl.get_bindings();
assert!(bindings.is_empty());
}
#[test]
fn test_repl_get_bindings_mut() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let bindings = repl.get_bindings_mut();
bindings.insert("test".to_string(), Value::Integer(42));
assert!(repl.get_bindings().contains_key("test"));
}
#[test]
fn test_repl_clear_bindings() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
repl.get_bindings_mut()
.insert("x".to_string(), Value::Integer(1));
assert!(!repl.get_bindings().is_empty());
repl.clear_bindings();
assert!(repl.get_bindings().is_empty());
}
#[test]
fn test_repl_get_evaluator_mut() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let evaluator = repl.get_evaluator_mut();
assert!(evaluator.is_some());
}
#[test]
fn test_repl_can_accept_input() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
assert!(repl.can_accept_input());
}
#[test]
fn test_repl_bindings_valid() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
assert!(repl.bindings_valid());
}
#[test]
fn test_repl_is_failed() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
assert!(!repl.is_failed());
}
#[test]
fn test_repl_recover() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
assert!(repl.recover().is_ok());
}
#[test]
fn test_repl_get_mode() {
let repl = Repl::new(std::env::temp_dir()).unwrap();
assert_eq!(repl.get_mode(), "normal");
}
#[test]
fn test_repl_handle_command() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let result = repl.handle_command("2 + 2");
assert_eq!(result, "Command executed");
}
#[test]
fn test_repl_eval_bounded() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let result = repl
.eval_bounded("1 + 1", 1024 * 1024, Duration::from_secs(5))
.unwrap();
assert_eq!(result, "2");
}
#[test]
fn test_repl_eval_transactional_success() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
let result = repl.eval_transactional("5 * 5").unwrap();
assert_eq!(result, "25");
}
#[test]
fn test_repl_eval_transactional_rollback() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
repl.eval("let x = 10").unwrap();
let result = repl.eval_transactional("undefined_var");
assert!(result.is_err());
assert!(repl.get_bindings().contains_key("x"));
}
#[test]
fn test_repl_result_history_tracking() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
assert_eq!(repl.result_history_len(), 0);
repl.eval("1").unwrap();
assert_eq!(repl.result_history_len(), 1);
repl.eval("2").unwrap();
assert_eq!(repl.result_history_len(), 2);
}
#[test]
fn test_repl_let_binding() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
repl.eval("let answer = 42").unwrap();
assert!(repl.get_bindings().contains_key("answer"));
}
#[test]
fn test_repl_arithmetic() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
assert_eq!(repl.eval("10 + 5").unwrap(), "15");
assert_eq!(repl.eval("10 - 3").unwrap(), "7");
assert_eq!(repl.eval("4 * 5").unwrap(), "20");
assert_eq!(repl.eval("20 / 4").unwrap(), "5");
}
#[test]
fn test_repl_comparison() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
assert_eq!(repl.eval("5 > 3").unwrap(), "true");
assert_eq!(repl.eval("3 < 5").unwrap(), "true");
assert_eq!(repl.eval("5 == 5").unwrap(), "true");
assert_eq!(repl.eval("5 != 3").unwrap(), "true");
}
#[test]
fn test_repl_boolean_logic() {
let mut repl = Repl::new(std::env::temp_dir()).unwrap();
assert_eq!(repl.eval("true && true").unwrap(), "true");
assert_eq!(repl.eval("true || false").unwrap(), "true");
assert_eq!(repl.eval("!false").unwrap(), "true");
}
}