mathexpr 0.1.1

A fast, safe mathematical expression parser and evaluator with bytecode compilation
Documentation
//! Interactive calculator REPL for mathexpr.
//!
//! Run with: cargo run --example repl
//!
//! Features:
//! - Evaluate mathematical expressions
//! - Define variables: `x = 5`
//! - Use variables in expressions: `x^2 + 1`
//! - Built-in constants: `pi`, `e`
//! - 27 built-in functions: sqrt, sin, cos, log, etc.
//! - Command history (up/down arrows)
//! - Line editing (left/right, backspace, etc.)
//!
//! Commands:
//! - `vars` - List all defined variables
//! - `help` - Show available functions
//! - `quit` or `exit` - Exit the REPL

use mathexpr::Expression;
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
use std::collections::HashMap;

const HISTORY_FILE: &str = ".mathexpr_history";

fn main() {
    println!("mathexpr calculator");
    println!("Type 'help' for available functions, 'quit' to exit.\n");

    let mut rl = match DefaultEditor::new() {
        Ok(rl) => rl,
        Err(e) => {
            eprintln!("Failed to initialize readline: {}", e);
            return;
        }
    };

    // Load history from file (ignore errors if file doesn't exist)
    let _ = rl.load_history(HISTORY_FILE);

    let mut variables: HashMap<String, f64> = HashMap::new();

    loop {
        match rl.readline("mathexpr> ") {
            Ok(line) => {
                let input = line.trim();
                if input.is_empty() {
                    continue;
                }

                // Add to history
                let _ = rl.add_history_entry(&line);

                // Handle commands
                match input.to_lowercase().as_str() {
                    "quit" | "exit" => {
                        println!("Goodbye!");
                        break;
                    }
                    "vars" => {
                        print_variables(&variables);
                        continue;
                    }
                    "help" => {
                        print_help();
                        continue;
                    }
                    _ => {}
                }

                // Check for variable assignment: `name = expression`
                if let Some((name, expr)) = parse_assignment(input) {
                    match evaluate_expression(expr, &variables) {
                        Ok(value) => {
                            println!("{}", value);
                            variables.insert(name.to_string(), value);
                        }
                        Err(e) => println!("Error: {}", e),
                    }
                } else {
                    // Evaluate as expression
                    match evaluate_expression(input, &variables) {
                        Ok(value) => println!("{}", value),
                        Err(e) => println!("Error: {}", e),
                    }
                }
            }
            Err(ReadlineError::Interrupted) => {
                // Ctrl+C - just print a new line and continue
                println!();
                continue;
            }
            Err(ReadlineError::Eof) => {
                // Ctrl+D - exit
                println!("Goodbye!");
                break;
            }
            Err(e) => {
                eprintln!("Error: {}", e);
                break;
            }
        }
    }

    // Save history to file
    let _ = rl.save_history(HISTORY_FILE);
}

/// Parse an assignment like `x = 5 + 3` into (name, expression).
fn parse_assignment(input: &str) -> Option<(&str, &str)> {
    // Find the first '=' that's not part of '==' (though we don't support ==)
    let eq_pos = input.find('=')?;

    // The name is everything before '='
    let name = input[..eq_pos].trim();

    // Must be a valid identifier (alphanumeric + underscore, not starting with digit)
    if name.is_empty() || !is_valid_identifier(name) {
        return None;
    }

    // Don't allow reassigning constants
    if name == "pi" || name == "e" {
        return None;
    }

    // The expression is everything after '='
    let expr = input[eq_pos + 1..].trim();
    if expr.is_empty() {
        return None;
    }

    Some((name, expr))
}

/// Check if a string is a valid identifier.
fn is_valid_identifier(s: &str) -> bool {
    let mut chars = s.chars();
    match chars.next() {
        Some(c) if c.is_alphabetic() || c == '_' => {}
        _ => return false,
    }
    chars.all(|c| c.is_alphanumeric() || c == '_')
}

/// Evaluate an expression with the current variables.
fn evaluate_expression(
    expr: &str,
    variables: &HashMap<String, f64>,
) -> Result<f64, Box<dyn std::error::Error>> {
    // Parse the expression
    let parsed = Expression::parse(expr)?;

    // Collect variable names and values in consistent order
    let var_names: Vec<&str> = variables.keys().map(|s| s.as_str()).collect();
    let var_values: Vec<f64> = var_names.iter().map(|name| variables[*name]).collect();

    // Compile with our variables
    let compiled = parsed.compile(&var_names)?;

    // Evaluate
    let result = compiled.eval(&var_values)?;
    Ok(result)
}

/// Print all defined variables.
fn print_variables(variables: &HashMap<String, f64>) {
    if variables.is_empty() {
        println!("No variables defined.");
    } else {
        let mut vars: Vec<_> = variables.iter().collect();
        vars.sort_by_key(|(k, _)| *k);
        for (name, value) in vars {
            println!("  {} = {}", name, value);
        }
    }
}

/// Print help information.
fn print_help() {
    println!("mathexpr - Mathematical Expression Calculator\n");
    println!("USAGE:");
    println!("  <expression>        Evaluate an expression");
    println!("  <name> = <expr>     Define a variable\n");
    println!("COMMANDS:");
    println!("  vars                List all defined variables");
    println!("  help                Show this help message");
    println!("  quit, exit          Exit the calculator\n");
    println!("OPERATORS:");
    println!("  +  -  *  /          Basic arithmetic");
    println!("  %                   Modulo");
    println!("  ^                   Exponentiation (right-associative)\n");
    println!("CONSTANTS:");
    println!("  pi                  3.14159...");
    println!("  e                   2.71828...\n");
    println!("FUNCTIONS:");
    println!("  Core:    abs, sqrt, cbrt, exp, log/ln, log2, log10, pow, mod");
    println!("  Trig:    sin, cos, tan, asin, acos, atan");
    println!("  Hyper:   sinh, cosh, tanh");
    println!("  Round:   floor, ceil, round, trunc, signum");
    println!("  Bounds:  min, max, clamp\n");
    println!("EXAMPLES:");
    println!("  2 + 3 * 4           => 14");
    println!("  sqrt(3^2 + 4^2)     => 5");
    println!("  x = 10              => 10");
    println!("  2 * pi * x          => 62.83...");
}