use rustyline::error::ReadlineError; use rustyline::DefaultEditor; use std::env; use std::path::{Path, PathBuf}; use std::process::{Command, ExitCode}; fn repo_root() -> Result { env::current_dir().map_err(|err| format!("failed to get current directory: {err}")) } fn mlang_binary(root: &Path) -> PathBuf { root.join(".lake").join("build").join("bin").join("mlang") } fn eval(binary: &Path, input: &str) -> Result<(), String> { let output = Command::new(binary) .arg("--eval") .arg(input) .output() .map_err(|err| format!("failed to run {}: {err}", binary.display()))?; if !output.stdout.is_empty() { print!("{}", String::from_utf8_lossy(&output.stdout)); } if !output.stderr.is_empty() { eprint!("{}", String::from_utf8_lossy(&output.stderr)); } if output.status.success() { Ok(()) } else { Err(format!("mlang exited with {}", output.status)) } } fn parse_bare_let_binding(input: &str) -> Option<(&str, &str)> { let rest = input.strip_prefix("let ")?; let eq_idx = rest.find('=')?; let name = rest[..eq_idx].trim(); if name.is_empty() || !name .chars() .all(|c| c == '_' || c.is_ascii_alphanumeric()) || !name .chars() .next() .is_some_and(|c| c == '_' || c.is_ascii_alphabetic()) { return None; } let value = rest[eq_idx + 1..].trim(); if value.is_empty() || input.contains(';') { return None; } Some((name, value)) } fn render_repl_input(prelude: &[String], input: &str) -> String { let body = match parse_bare_let_binding(input) { Some((name, value)) => format!("let {name} = {value} in {name}"), None => input.to_owned(), }; if prelude.is_empty() { body } else { let mut rendered = prelude .iter() .rev() .fold(body, |acc, binding| format!("{binding} in {acc}")); rendered.shrink_to_fit(); rendered } } fn main() -> ExitCode { let root = match repo_root() { Ok(root) => root, Err(err) => { eprintln!("{err}"); return ExitCode::FAILURE; } }; let binary = mlang_binary(&root); if !binary.is_file() { eprintln!("missing evaluator binary at {}", binary.display()); return ExitCode::FAILURE; } let mut rl = match DefaultEditor::new() { Ok(rl) => rl, Err(err) => { eprintln!("failed to initialize line editor: {err}"); return ExitCode::FAILURE; } }; println!("mlang REPL. enter :quit to exit."); let mut prelude: Vec = Vec::new(); loop { match rl.readline("mlang> ") { Ok(line) => { let input = line.trim(); if input.is_empty() { continue; } if input == ":quit" || input == ":q" { return ExitCode::SUCCESS; } let _ = rl.add_history_entry(input); let rendered = render_repl_input(&prelude, input); if eval(&binary, &rendered).is_ok() { if let Some((name, value)) = parse_bare_let_binding(input) { prelude.push(format!("let {name} = {value}")); } } } Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { println!(); return ExitCode::SUCCESS; } Err(err) => { eprintln!("readline error: {err}"); return ExitCode::FAILURE; } } } }