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:

  1. Read: Input is read from the user
  2. Eval: Input is evaluated
  3. Print: Result is printed
  4. 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 CodeMeaning
0Success
64Usage error (invalid arguments)
65Data error (lexer/parser error)
66Cannot open input
70Software 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:

FeatureDescription
REPLInteractive shell with command support
File runnerExecute .lux files
CLI options--help, --version, --dump-*
Exit codesProper UNIX exit codes
Error recoveryREPL 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