Add enhanced REPL with rustyline

REPL improvements:
- Tab completion for keywords, commands, and user definitions
- Persistent history saved to ~/.lux_history
- Better line editing with Emacs keybindings
- Ctrl-C to cancel input, Ctrl-D to exit
- History search with Ctrl-R

New commands:
- :info <name> - Show type information for a binding
- :env - List user-defined bindings with types

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 04:32:52 -05:00
parent db516f5cff
commit 052db9c88f
2 changed files with 300 additions and 54 deletions

View File

@@ -14,7 +14,15 @@ mod types;
use diagnostics::render; use diagnostics::render;
use interpreter::Interpreter; use interpreter::Interpreter;
use parser::Parser; use parser::Parser;
use std::io::{self, Write}; use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::history::DefaultHistory;
use rustyline::validate::Validator;
use rustyline::{Config, Editor, Helper};
use std::borrow::Cow;
use std::collections::HashSet;
use typechecker::TypeChecker; use typechecker::TypeChecker;
const VERSION: &str = "0.1.0"; const VERSION: &str = "0.1.0";
@@ -26,28 +34,33 @@ Commands:
:help, :h Show this help :help, :h Show this help
:quit, :q Exit the REPL :quit, :q Exit the REPL
:type <expr> Show the type of an expression :type <expr> Show the type of an expression
:info <name> Show info about a binding
:env Show user-defined bindings
:clear Clear the environment :clear Clear the environment
:load <file> Load and execute a file :load <file> Load and execute a file
:trace on/off Enable/disable effect tracing :trace on/off Enable/disable effect tracing
:traces Show recorded effect traces :traces Show recorded effect traces
Keyboard:
Tab Autocomplete
Ctrl-C Cancel current input
Ctrl-D Exit
Up/Down Browse history
Ctrl-R Search history
Examples: Examples:
> let x = 42 > let x = 42
> x + 1 > x + 1
43 43
> fn double(n: Int): Int = n * 2 > fn double(n: Int): Int = n * 2
> :type double
double : fn(Int) -> Int
> double(21) > double(21)
42 42
> Console.print("Hello, world!") > match Some(5) { Some(x) => x, None => 0 }
Hello, world! 5
Debugging:
> :trace on
> Console.print("test")
> :traces
[ 0.123ms] Console.print("test") → ()
"#; "#;
fn main() { fn main() {
@@ -114,45 +127,177 @@ fn run_file(path: &str) {
} }
} }
/// REPL helper for tab completion and syntax highlighting
struct LuxHelper {
keywords: HashSet<String>,
commands: Vec<String>,
user_defined: HashSet<String>,
}
impl LuxHelper {
fn new() -> Self {
let keywords: HashSet<String> = vec![
"fn", "let", "if", "then", "else", "match", "with", "effect", "handler",
"handle", "resume", "type", "import", "pub", "is", "pure", "total",
"idempotent", "deterministic", "commutative", "where", "assume", "true",
"false", "None", "Some", "Ok", "Err", "Int", "Float", "String", "Bool",
"Char", "Unit", "Option", "Result", "List",
]
.into_iter()
.map(String::from)
.collect();
let commands = vec![
":help", ":h", ":quit", ":q", ":type", ":t", ":clear", ":load", ":l",
":trace", ":traces", ":info", ":i", ":env",
]
.into_iter()
.map(String::from)
.collect();
Self {
keywords,
commands,
user_defined: HashSet::new(),
}
}
fn add_definition(&mut self, name: &str) {
self.user_defined.insert(name.to_string());
}
}
impl Completer for LuxHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Pair>)> {
// Find the start of the current word
let start = line[..pos]
.rfind(|c: char| c.is_whitespace() || "(){}[],.;:".contains(c))
.map(|i| i + 1)
.unwrap_or(0);
let prefix = &line[start..pos];
if prefix.is_empty() {
return Ok((pos, Vec::new()));
}
let mut completions = Vec::new();
// Complete commands
if prefix.starts_with(':') {
for cmd in &self.commands {
if cmd.starts_with(prefix) {
completions.push(Pair {
display: cmd.clone(),
replacement: cmd.clone(),
});
}
}
} else {
// Complete keywords
for kw in &self.keywords {
if kw.starts_with(prefix) {
completions.push(Pair {
display: kw.clone(),
replacement: kw.clone(),
});
}
}
// Complete user-defined names
for name in &self.user_defined {
if name.starts_with(prefix) {
completions.push(Pair {
display: name.clone(),
replacement: name.clone(),
});
}
}
}
Ok((start, completions))
}
}
impl Hinter for LuxHelper {
type Hint = String;
fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option<String> {
None
}
}
impl Highlighter for LuxHelper {
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
}
}
impl Validator for LuxHelper {}
impl Helper for LuxHelper {}
fn get_history_path() -> Option<std::path::PathBuf> {
std::env::var_os("HOME").map(|home| {
std::path::PathBuf::from(home).join(".lux_history")
})
}
fn run_repl() { fn run_repl() {
println!("Lux v{}", VERSION); println!("Lux v{}", VERSION);
println!("Type :help for help, :quit to exit\n"); println!("Type :help for help, :quit to exit\n");
let config = Config::builder()
.history_ignore_space(true)
.completion_type(rustyline::CompletionType::List)
.edit_mode(rustyline::EditMode::Emacs)
.build();
let helper = LuxHelper::new();
let mut rl: Editor<LuxHelper, DefaultHistory> = Editor::with_config(config).unwrap();
rl.set_helper(Some(helper));
// Load history
if let Some(history_path) = get_history_path() {
if let Some(parent) = history_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = rl.load_history(&history_path);
}
let mut interp = Interpreter::new(); let mut interp = Interpreter::new();
let mut checker = TypeChecker::new(); let mut checker = TypeChecker::new();
let mut buffer = String::new(); let mut buffer = String::new();
let mut continuation = false; let mut continuation = false;
loop { loop {
// Print prompt
let prompt = if continuation { "... " } else { "lux> " }; let prompt = if continuation { "... " } else { "lux> " };
print!("{}", prompt);
io::stdout().flush().unwrap();
// Read input match rl.readline(prompt) {
let mut line = String::new(); Ok(line) => {
match io::stdin().read_line(&mut line) { let line = line.trim_end().to_string();
Ok(0) => break, // EOF
Ok(_) => {}
Err(e) => {
eprintln!("Error reading input: {}", e);
continue;
}
}
let line = line.trim_end(); // Don't add empty lines or continuations to history
if !line.is_empty() && !continuation {
let _ = rl.add_history_entry(line.as_str());
}
// Handle commands // Handle commands
if !continuation && line.starts_with(':') { if !continuation && line.starts_with(':') {
handle_command(line, &mut interp, &mut checker); handle_command(&line, &mut interp, &mut checker, rl.helper_mut().unwrap());
continue; continue;
} }
// Accumulate input // Accumulate input
buffer.push_str(line); buffer.push_str(&line);
buffer.push('\n'); buffer.push('\n');
// Check for continuation (simple heuristic: unbalanced braces) // Check for continuation (unbalanced braces/parens)
let open_braces = buffer.chars().filter(|c| *c == '{').count(); let open_braces = buffer.chars().filter(|c| *c == '{').count();
let close_braces = buffer.chars().filter(|c| *c == '}').count(); let close_braces = buffer.chars().filter(|c| *c == '}').count();
let open_parens = buffer.chars().filter(|c| *c == '(').count(); let open_parens = buffer.chars().filter(|c| *c == '(').count();
@@ -170,13 +315,67 @@ fn run_repl() {
continue; continue;
} }
// Track new definitions for completion
if let Some(name) = extract_definition_name(&input) {
rl.helper_mut().unwrap().add_definition(&name);
}
eval_input(&input, &mut interp, &mut checker); eval_input(&input, &mut interp, &mut checker);
} }
Err(ReadlineError::Interrupted) => {
// Ctrl-C: clear buffer
buffer.clear();
continuation = false;
println!("^C");
}
Err(ReadlineError::Eof) => {
// Ctrl-D: exit
break;
}
Err(err) => {
eprintln!("Error: {:?}", err);
break;
}
}
}
// Save history
if let Some(history_path) = get_history_path() {
let _ = rl.save_history(&history_path);
}
println!("\nGoodbye!"); println!("\nGoodbye!");
} }
fn handle_command(line: &str, interp: &mut Interpreter, checker: &mut TypeChecker) { fn extract_definition_name(input: &str) -> Option<String> {
let input = input.trim();
if input.starts_with("fn ") {
input.split_whitespace().nth(1).and_then(|s| {
s.split('(').next().map(String::from)
})
} else if input.starts_with("let ") {
input.split_whitespace().nth(1).and_then(|s| {
s.split([':', '=']).next().map(|s| s.trim().to_string())
})
} else if input.starts_with("type ") {
input.split_whitespace().nth(1).and_then(|s| {
s.split(['<', '=']).next().map(|s| s.trim().to_string())
})
} else if input.starts_with("effect ") {
input.split_whitespace().nth(1).map(|s| {
s.trim_end_matches('{').trim().to_string()
})
} else {
None
}
}
fn handle_command(
line: &str,
interp: &mut Interpreter,
checker: &mut TypeChecker,
helper: &mut LuxHelper,
) {
let parts: Vec<&str> = line.splitn(2, ' ').collect(); let parts: Vec<&str> = line.splitn(2, ' ').collect();
let cmd = parts[0]; let cmd = parts[0];
let arg = parts.get(1).map(|s| s.trim()); let arg = parts.get(1).map(|s| s.trim());
@@ -196,14 +395,25 @@ fn handle_command(line: &str, interp: &mut Interpreter, checker: &mut TypeChecke
println!("Usage: :type <expression>"); println!("Usage: :type <expression>");
} }
} }
":info" | ":i" => {
if let Some(name) = arg {
show_info(name, checker);
} else {
println!("Usage: :info <name>");
}
}
":env" => {
show_environment(checker, helper);
}
":clear" => { ":clear" => {
*interp = Interpreter::new(); *interp = Interpreter::new();
*checker = TypeChecker::new(); *checker = TypeChecker::new();
helper.user_defined.clear();
println!("Environment cleared."); println!("Environment cleared.");
} }
":load" | ":l" => { ":load" | ":l" => {
if let Some(path) = arg { if let Some(path) = arg {
load_file(path, interp, checker); load_file(path, interp, checker, helper);
} else { } else {
println!("Usage: :load <filename>"); println!("Usage: :load <filename>");
} }
@@ -235,6 +445,30 @@ fn handle_command(line: &str, interp: &mut Interpreter, checker: &mut TypeChecke
} }
} }
fn show_info(name: &str, checker: &TypeChecker) {
// Look up in the type environment
if let Some(scheme) = checker.lookup(name) {
println!("{} : {}", name, scheme);
} else {
println!("'{}' is not defined.", name);
}
}
fn show_environment(checker: &TypeChecker, helper: &LuxHelper) {
println!("User-defined bindings:");
if helper.user_defined.is_empty() {
println!(" (none)");
} else {
for name in &helper.user_defined {
if let Some(scheme) = checker.lookup(name) {
println!(" {} : {}", name, scheme);
} else {
println!(" {} : <unknown>", name);
}
}
}
}
fn show_type(expr_str: &str, checker: &mut TypeChecker) { fn show_type(expr_str: &str, checker: &mut TypeChecker) {
// Wrap expression in a let to parse it // Wrap expression in a let to parse it
let wrapped = format!("let _expr_ = {}", expr_str); let wrapped = format!("let _expr_ = {}", expr_str);
@@ -255,7 +489,7 @@ fn show_type(expr_str: &str, checker: &mut TypeChecker) {
} }
} }
fn load_file(path: &str, interp: &mut Interpreter, checker: &mut TypeChecker) { fn load_file(path: &str, interp: &mut Interpreter, checker: &mut TypeChecker, helper: &mut LuxHelper) {
let source = match std::fs::read_to_string(path) { let source = match std::fs::read_to_string(path) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
@@ -279,6 +513,13 @@ fn load_file(path: &str, interp: &mut Interpreter, checker: &mut TypeChecker) {
return; return;
} }
// Add definitions for completion
for decl in &program.declarations {
if let Some(name) = extract_definition_name(&format!("{:?}", decl)) {
helper.add_definition(&name);
}
}
match interp.run(&program) { match interp.run(&program) {
Ok(_) => println!("Loaded '{}'", path), Ok(_) => println!("Loaded '{}'", path),
Err(e) => println!("Runtime error: {}", e), Err(e) => println!("Runtime error: {}", e),

View File

@@ -114,6 +114,11 @@ impl TypeChecker {
} }
} }
/// Look up a type scheme by name (for REPL :info command)
pub fn lookup(&self, name: &str) -> Option<&TypeScheme> {
self.env.bindings.get(name)
}
/// Type check a program /// Type check a program
pub fn check_program(&mut self, program: &Program) -> Result<(), Vec<TypeError>> { pub fn check_program(&mut self, program: &Program) -> Result<(), Vec<TypeError>> {
// First pass: collect all declarations // First pass: collect all declarations