REPL & File Runner
The REPL (Read-Eval-Print Loop) provides an interactive environment for exploring Lux. Combined with a file runner, users have two ways to execute Lux code.
What is a REPL?
A REPL is an interactive shell where you can:
- Read: Input is read from the user
- Eval: Input is evaluated
- Print: Result is printed
- Loop: Repeat
Example session:
Lux 0.1.0
>>> let x = 5
>>> let y = 10
>>> print x + y
15
>>> fn greet(name) { return "Hello, " + name; }
>>> greet("Alice")
"Hello, Alice"
>>> :quit
Goodbye!
REPL Features
Our REPL will support:
- Multi-line input (auto-detect incomplete expressions)
- Persistent environment across inputs
- Special commands (
:help,:quit,:clear,:env) - Error recovery (continue after errors)
- History (optional, with rustyline)
Building the REPL
Update src/main.rs:
mod ast;
mod lexer;
mod parser;
mod environment;
mod interpreter;
mod error;
use std::io::{self, Write};
use lexer::Lexer;
use parser::Parser;
use interpreter::Interpreter;
use error::LuxError;
fn main() {
let args: Vec<String> = std::env::args().collect();
match args.len() {
1 => run_repl(),
2 => run_file(&args[1]),
_ => {
eprintln!("Usage: lux [file.lux]");
std::process::exit(1);
}
}
}
// ========== REPL ==========
fn run_repl() {
println!("Lux 0.1.0");
println!("Type :help for help, :quit to exit");
println!();
let mut interpreter = Interpreter::new();
let mut line_number = 1;
loop {
// Print prompt
print!(">>> ");
io::stdout().flush().unwrap();
// Read input
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(0) => break, // EOF (Ctrl+D)
Ok(_) => {}
Err(e) => {
eprintln!("Error reading input: {}", e);
continue;
}
}
let input = input.trim();
// Handle empty input
if input.is_empty() {
continue;
}
// Handle special commands
if input.starts_with(':') {
handle_repl_command(input, &interpreter);
continue;
}
// Execute Lux code
if let Err(e) = execute_repl_input(input, &mut interpreter, line_number) {
eprintln!("{}", e);
}
line_number += 1;
}
println!("\nGoodbye!");
}
fn execute_repl_input(
source: &str,
interpreter: &mut Interpreter,
line_number: usize,
) -> Result<(), String> {
// Lexing
let mut lexer = Lexer::new(source.to_string());
let tokens = match lexer.tokenize() {
Ok(t) => t,
Err(e) => return Err(format!("Lexer error: {}", e.message)),
};
// Parsing
let mut parser = Parser::new(tokens);
let statements = match parser.parse() {
Ok(s) => s,
Err(e) => return Err(format!("Parser error: {}", e.message)),
};
// Interpreting
match interpreter.interpret(statements) {
Ok(_) => Ok(()),
Err(e) => Err(format!("Runtime error: {}", e.message)),
}
}
fn handle_repl_command(command: &str, interpreter: &Interpreter) {
match command {
":help" | ":h" => {
println!("Available commands:");
println!(" :help, :h - Show this help message");
println!(" :quit, :q - Exit the REPL");
println!(" :clear, :c - Clear all variables");
println!(" :env, :e - Show current environment");
println!();
println!("Type Lux code directly to execute it.");
println!("Example: let x = 5");
}
":quit" | ":q" => {
println!("Goodbye!");
std::process::exit(0);
}
":clear" | ":c" => {
println!("Environment cleared");
println!("Note: Restart the REPL to fully clear state");
}
":env" | ":e" => {
println!("Current environment:");
println!(" (Environment inspection not yet implemented)");
println!(" Native functions: clock, len, type, push, pop, str, num, input");
}
_ => {
println!("Unknown command: {}", command);
println!("Type :help for available commands");
}
}
}
// ========== File Runner ==========
fn run_file(path: &str) {
// Read file
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to read file '{}': {}", path, e);
std::process::exit(1);
}
};
// Execute
match run_source(&source) {
Ok(_) => {}
Err(e) => {
eprintln!("{}", e.with_source(&source).display());
std::process::exit(1);
}
}
}
fn run_source(source: &str) -> Result<(), LuxError> {
// Lexing
let mut lexer = Lexer::new(source.to_string());
let tokens = lexer.tokenize()?;
// Parsing
let mut parser = Parser::new(tokens);
let statements = parser.parse()?;
// Interpreting
let mut interpreter = Interpreter::new();
interpreter.interpret(statements)?;
Ok(())
}
Multi-Line Input
For a better REPL experience, detect incomplete input:
fn is_complete_input(input: &str) -> bool {
let mut brace_count = 0;
let mut paren_count = 0;
let mut bracket_count = 0;
let mut in_string = false;
let mut escape_next = false;
for ch in input.chars() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' if in_string => escape_next = true,
'"' => in_string = !in_string,
'{' if !in_string => brace_count += 1,
'}' if !in_string => brace_count -= 1,
'(' if !in_string => paren_count += 1,
')' if !in_string => paren_count -= 1,
'[' if !in_string => bracket_count += 1,
']' if !in_string => bracket_count -= 1,
_ => {}
}
}
brace_count == 0 && paren_count == 0 && bracket_count == 0 && !in_string
}
fn read_complete_input() -> Result<String, io::Error> {
let mut complete_input = String::new();
let mut first_line = true;
loop {
if first_line {
print!(">>> ");
first_line = false;
} else {
print!("... ");
}
io::stdout().flush()?;
let mut line = String::new();
match io::stdin().read_line(&mut line)? {
0 => break, // EOF
_ => {
complete_input.push_str(&line);
if is_complete_input(&complete_input.trim()) {
break;
}
}
}
}
Ok(complete_input)
}
Usage in REPL:
>>> fn factorial(n) {
... if n <= 1 {
... return 1;
... }
... return n * factorial(n - 1);
... }
>>> print factorial(5)
120
Enhanced REPL with Rustyline
For professional line editing, history, and completion, use the rustyline crate:
Update Cargo.toml:
[dependencies]
rustyline = "12.0"
Enhanced REPL:
use rustyline::error::ReadlineError;
use rustyline::{DefaultEditor, Result as RustylineResult};
fn run_repl_with_rustyline() -> RustylineResult<()> {
println!("Lux 0.1.0");
println!("Type :help for help, :quit to exit");
println!();
let mut rl = DefaultEditor::new()?;
let mut interpreter = Interpreter::new();
// Load history
let _ = rl.load_history("lux_history.txt");
loop {
let readline = rl.readline(">>> ");
match readline {
Ok(line) => {
let input = line.trim();
if input.is_empty() {
continue;
}
rl.add_history_entry(input)?;
// Handle commands or execute code
if input.starts_with(':') {
handle_repl_command(input, &interpreter);
} else {
if let Err(e) = execute_repl_input(input, &mut interpreter, 0) {
eprintln!("{}", e);
}
}
}
Err(ReadlineError::Interrupted) => {
println!("Interrupted (Ctrl+C)");
break;
}
Err(ReadlineError::Eof) => {
println!("EOF (Ctrl+D)");
break;
}
Err(err) => {
eprintln!("Error: {:?}", err);
break;
}
}
}
// Save history
rl.save_history("lux_history.txt")?;
println!("Goodbye!");
Ok(())
}
Features gained:
- Line editing: Arrow keys, Ctrl+A/E, etc.
- History: Up/down arrows to navigate
- Persistence: History saved between sessions
- Tab completion: (with custom completer)
Command-Line Arguments
Support various CLI options:
use std::env;
struct Config {
mode: Mode,
dump_tokens: bool,
dump_ast: bool,
}
enum Mode {
Repl,
File(String),
}
fn parse_args() -> Result<Config, String> {
let args: Vec<String> = env::args().collect();
let mut config = Config {
mode: Mode::Repl,
dump_tokens: false,
dump_ast: false,
};
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--dump-tokens" => config.dump_tokens = true,
"--dump-ast" => config.dump_ast = true,
"--help" => {
print_help();
std::process::exit(0);
}
"--version" => {
println!("Lux 0.1.0");
std::process::exit(0);
}
arg if arg.starts_with('-') => {
return Err(format!("Unknown option: {}", arg));
}
file => {
config.mode = Mode::File(file.to_string());
}
}
i += 1;
}
Ok(config)
}
fn print_help() {
println!("Lux 0.1.0");
println!();
println!("USAGE:");
println!(" lux [OPTIONS] [FILE]");
println!();
println!("OPTIONS:");
println!(" --dump-tokens Print tokens after lexing");
println!(" --dump-ast Print AST after parsing");
println!(" --help Print this help message");
println!(" --version Print version");
println!();
println!("If no FILE is provided, starts the REPL.");
}
fn main() {
let config = match parse_args() {
Ok(c) => c,
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
};
match config.mode {
Mode::Repl => run_repl(),
Mode::File(path) => run_file_with_config(&path, &config),
}
}
Exit Codes
Return appropriate exit codes:
fn run_file(path: &str) -> i32 {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to read file '{}': {}", path, e);
return 66; // EX_NOINPUT
}
};
match run_source(&source) {
Ok(_) => 0, // Success
Err(e) => {
eprintln!("{}", e.with_source(&source).display());
match e.error_type {
ErrorType::Lexer | ErrorType::Parser => 65, // EX_DATAERR
ErrorType::Runtime => 70, // EX_SOFTWARE
}
}
}
}
fn main() {
let exit_code = match parse_args() {
Ok(config) => match config.mode {
Mode::Repl => {
run_repl();
0
}
Mode::File(path) => run_file(&path),
},
Err(e) => {
eprintln!("Error: {}", e);
64 // EX_USAGE
}
};
std::process::exit(exit_code);
}
| Exit Code | Meaning |
|---|---|
| 0 | Success |
| 64 | Usage error (invalid arguments) |
| 65 | Data error (lexer/parser error) |
| 66 | Cannot open input |
| 70 | Software error (runtime error) |
Example Lux Programs
Create examples/hello.lux:
print "Hello, World!";
Create examples/fibonacci.lux:
fn fibonacci(n) {
if n <= 1 {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
print "Fibonacci sequence:";
let i = 0;
while i <= 10 {
print "fib(" + str(i) + ") = " + str(fibonacci(i));
i = i + 1;
}
Create examples/counter.lux:
fn make_counter() {
let count = 0;
return fn() {
count = count + 1;
return count;
};
}
let counter = make_counter();
print counter(); // 1
print counter(); // 2
print counter(); // 3
Run them:
cargo run -- examples/hello.lux
cargo run -- examples/fibonacci.lux
cargo run -- examples/counter.lux
REPL Session Examples
Example 1: Variables and Arithmetic
>>> let x = 10
>>> let y = 20
>>> print x + y
30
>>> let z = x * y
>>> print z
200
Example 2: Functions
>>> fn greet(name) { return "Hello, " + name + "!"; }
>>> print greet("Alice")
Hello, Alice!
>>> print greet("Bob")
Hello, Bob!
Example 3: Lists
>>> let numbers = [1, 2, 3, 4, 5]
>>> for n in numbers { print n * n; }
1
4
9
16
25
Example 4: Closures
>>> fn make_adder(x) { return fn(y) { return x + y; }; }
>>> let add5 = make_adder(5)
>>> print add5(10)
15
>>> print add5(20)
25
Environment Inspection
Add ability to inspect variables:
impl Environment {
pub fn list_variables(&self) -> Vec<(String, String)> {
let mut vars = Vec::new();
for (name, value) in &self.values {
vars.push((name.clone(), value.to_string()));
}
if let Some(parent) = &self.parent {
vars.extend(parent.borrow().list_variables());
}
vars
}
}
// In REPL command handler
":env" | ":e" => {
println!("Current environment:");
let vars = interpreter.get_environment_vars();
for (name, value) in vars {
println!(" {} = {}", name, value);
}
}
Debugging Flags
Implement --dump-tokens and --dump-ast:
fn run_file_with_config(path: &str, config: &Config) {
let source = std::fs::read_to_string(path).unwrap();
// Lex
let mut lexer = Lexer::new(source.clone());
let tokens = lexer.tokenize().unwrap();
if config.dump_tokens {
println!("=== TOKENS ===");
for token in &tokens {
println!("{:?}", token);
}
println!();
}
// Parse
let mut parser = Parser::new(tokens);
let statements = parser.parse().unwrap();
if config.dump_ast {
println!("=== AST ===");
for stmt in &statements {
println!("{:#?}", stmt);
}
println!();
}
// Interpret
let mut interpreter = Interpreter::new();
interpreter.interpret(statements).unwrap();
}
Usage:
cargo run -- --dump-tokens examples/hello.lux
cargo run -- --dump-ast examples/fibonacci.lux
Summary
We now have:
| Feature | Description |
|---|---|
| REPL | Interactive shell with command support |
| File runner | Execute .lux files |
| CLI options | --help, --version, --dump-* |
| Exit codes | Proper UNIX exit codes |
| Error recovery | REPL continues after errors |
| History | (with rustyline) Command history |
What's Next?
Lux is functional! Now let's test it thoroughly and add debugging tools.
→ Continue to Chapter 9: Testing & Debugging