Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 55 additions & 146 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,15 @@ A mathematical expression evaluator library written in Rust with support for cus

## How It Works

The library implements a classic compiler pipeline:
Classic compiler pipeline with type-safe state transitions:

```
Source → Lexer → Parser → AST → Semantic Analysis → IR → Bytecode → VM
Input → Lexer → Parser → Compiler → Program<Compiled>
↓ link
Program<Linked> → Execute
```

1. **Lexer** - Tokenizes the input string into operators, numbers, and identifiers
2. **Parser** - Uses operator precedence climbing to build an Abstract Syntax Tree (AST)
3. **Semantic Analysis** - Resolves symbols and validates function arities
4. **IR Builder** - Converts the AST into stack-based bytecode instructions
5. **Virtual Machine** - Executes the bytecode on a stack-based VM

This architecture allows for:
- Separating parsing from execution
- Compiling expressions once and running them multiple times
- Serializing compiled bytecode to disk for later use
The `Program` type uses Rust's type system to enforce correct usage at compile time. You cannot execute an unlinked program, and you cannot link a program twice.

## Usage

Expand All @@ -40,7 +33,7 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
expr-solver-lib = "1.0.3"
expr-solver-lib = "1.1.0"
```

### As a binary
Expand All @@ -49,166 +42,82 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
expr-solver-bin = "1.0.3"
expr-solver-bin = "1.1.0"
```

### Basic Example
### Quick Evaluation

```rust
use expr_solver::Eval;

fn main() {
// Quick one-liner evaluation
match Eval::evaluate("2+3*4") {
Ok(result) => println!("Result: {}", result),
Err(e) => eprintln!("Error: {}", e),
}

// Or create an evaluator instance for more control
let mut eval = Eval::new("sqrt(16) + pi");
match eval.run() {
Ok(result) => println!("Result: {}", result),
Err(e) => eprintln!("Error: {}", e),
}
}
use expr_solver::eval;

// Simple one-liner
let result = eval("2 + 3 * 4").unwrap();
assert_eq!(result.to_string(), "14");

// With built-in functions
let result = eval("sqrt(16) + sin(pi/2)").unwrap();
```

### Advanced Example
### Custom Symbols

```rust
use expr_solver::{Eval, SymTable};
use expr_solver::{eval_with_table, SymTable};
use rust_decimal_macros::dec;

fn main() {
// Create a custom symbol table
let mut table = SymTable::stdlib();
table.add_const("x", dec!(10)).unwrap();
table.add_func("double", 1, false, |args| Ok(args[0] * dec!(2))).unwrap();

// Evaluate with custom symbols
let mut eval = Eval::with_table("double(x) + sqrt(25)", table);
let result = eval.run().unwrap();
println!("Result: {}", result); // 25
}
let mut table = SymTable::stdlib();
table.add_const("x", dec!(10)).unwrap();
table.add_func("double", 1, false, |args| Ok(args[0] * dec!(2))).unwrap();

let result = eval_with_table("double(x)", table).unwrap();
assert_eq!(result, dec!(20));
```

### Compile and Execute
### Compile Once, Execute Many Times

```rust
use expr_solver::Eval;
use std::path::PathBuf;

fn main() {
// Compile expression to bytecode
let mut eval = Eval::new("2 + 3 * 4");
eval.compile_to_file(&PathBuf::from("expr.bin")).unwrap();

// Load and execute the compiled bytecode
let mut eval = Eval::new_from_file(PathBuf::from("expr.bin"));
let result = eval.run().unwrap();
println!("Result: {}", result); // 14
}
```

### Viewing Assembly
use expr_solver::{load, SymTable};
use rust_decimal_macros::dec;

You can inspect the generated bytecode as human-readable assembly:
// Compile expression
let program = load("x * 2 + y").unwrap();

```rust
use expr_solver::Eval;
// Execute with different values
let mut table = SymTable::new();
table.add_const("x", dec!(10)).unwrap();
table.add_const("y", dec!(5)).unwrap();

fn main() {
let mut eval = Eval::new("2 + 3 * 4");
println!("{}", eval.get_assembly().unwrap());
}
let linked = program.link(table).unwrap();
let result = linked.execute().unwrap(); // 25
```

Output:
```asm
; VERSION 1.0.2
0000 PUSH 2
0001 PUSH 3
0002 PUSH 4
0003 MUL
0004 ADD
```
## Precision

The assembly shows the stack-based bytecode instructions that will be executed by the VM.
Uses **128-bit `Decimal`** arithmetic for exact decimal calculations without floating-point errors.

## Precision and Data Types
## Built-in Functions

All calculations are performed using **128-bit `Decimal`** type from the `rust_decimal` crate, providing exact decimal arithmetic without floating-point errors.
| Category | Functions |
|----------------|---------------------------------------------------------------------------|
| **Arithmetic** | `abs`, `sign`, `floor`, `ceil`, `round`, `trunc`, `fract`, `mod`, `clamp` |
| **Trig** | `sin`, `cos`, `tan`, `asin`*, `acos`*, `atan`*, `atan2`* |
| **Hyperbolic** | `sinh`*, `cosh`*, `tanh`* |
| **Exp/Log** | `sqrt`, `cbrt`*, `pow`, `exp`, `exp2`*, `log`, `log2`*, `log10`, `hypot`* |
| **Variadic** | `min`, `max`, `sum`, `avg` (1+ args) |
| **Special** | `if(cond, then, else)` |

> **Note**: Some trigonometric and hyperbolic functions (`asin`, `acos`, `atan`, `atan2`, `sinh`, `cosh`, `tanh`, `cbrt`, `exp2`, `log2`, `hypot`) internally convert to/from `f64` for computation, which may introduce minor precision differences. All constants (`pi`, `e`, `tau`, `ln2`, `ln10`, `sqrt2`) are computed using native `Decimal` operations for maximum precision.
\* *Uses f64 internally, may have minor precision differences*

## Built-in Functions
## Built-in Constants

| Function | Arguments | Description | Notes |
|-----------------------------|-----------|------------------------------------------|---------------------------------|
| **Arithmetic** | | | |
| `abs(x)` | 1 | Absolute value | |
| `sign(x)` | 1 | Sign (-1, 0, or 1) | |
| `floor(x)` | 1 | Round down to integer | |
| `ceil(x)` | 1 | Round up to integer | |
| `round(x)` | 1 | Round to nearest integer | |
| `trunc(x)` | 1 | Truncate to integer | |
| `fract(x)` | 1 | Fractional part | |
| `mod(x, y)` | 2 | Remainder of x/y | |
| `clamp(x, min, max)` | 3 | Constrain value between bounds | |
| **Trigonometry** | | | |
| `sin(x)` | 1 | Sine | |
| `cos(x)` | 1 | Cosine | |
| `tan(x)` | 1 | Tangent | |
| `asin(x)` | 1 | Arcsine | Uses f64 internally |
| `acos(x)` | 1 | Arccosine | Uses f64 internally |
| `atan(x)` | 1 | Arctangent | Uses f64 internally |
| `atan2(y, x)` | 2 | Two-argument arctangent | Uses f64 internally |
| **Hyperbolic** | | | |
| `sinh(x)` | 1 | Hyperbolic sine | Uses f64 internally |
| `cosh(x)` | 1 | Hyperbolic cosine | Uses f64 internally |
| `tanh(x)` | 1 | Hyperbolic tangent | Uses f64 internally |
| **Exponential/Logarithmic** | | | |
| `sqrt(x)` | 1 | Square root | |
| `cbrt(x)` | 1 | Cube root | Uses f64 internally |
| `pow(x, y)` | 2 | x raised to power y | |
| `exp(x)` | 1 | e raised to power x | |
| `exp2(x)` | 1 | 2 raised to power x | Uses f64 internally |
| `log(x)` | 1 | Natural logarithm | |
| `log2(x)` | 1 | Base-2 logarithm | Uses f64 internally |
| `log10(x)` | 1 | Base-10 logarithm | |
| `hypot(x, y)` | 2 | Euclidean distance √(x²+y²) | Uses f64 internally |
| **Variadic** | | | |
| `min(x, ...)` | 1+ | Minimum value | Accepts any number of arguments |
| `max(x, ...)` | 1+ | Maximum value | Accepts any number of arguments |
| `sum(x, ...)` | 1+ | Sum of values | Accepts any number of arguments |
| `avg(x, ...)` | 1+ | Average of values | Accepts any number of arguments |
| **Special** | | | |
| `if(cond, t, f)` | 3 | Conditional: returns t if cond≠0, else f | |
`pi`, `e`, `tau`, `ln2`, `ln10`, `sqrt2`

## Built-in Constants
> All names are case-insensitive.

## Operators

| Constant | Value | Description |
|----------|-------|-------------|
| `pi` | 3.14159... | π (pi) |
| `e` | 2.71828... | Euler's number |
| `tau` | 6.28318... | 2π (tau) |
| `ln2` | 0.69314... | Natural logarithm of 2 |
| `ln10` | 2.30258... | Natural logarithm of 10 |
| `sqrt2` | 1.41421... | Square root of 2 |

> **Note**: All function and constant names are case-insensitive.

## Supported Operators

| Operator | Type | Associativity | Precedence | Description |
|----------|------|---------------|------------|-------------|
| `!` | Postfix Unary | Left | 6 | Factorial |
| `^` | Binary | Right | 5 | Exponentiation |
| `-` | Prefix Unary | Right | 4 | Negation |
| `*`, `/` | Binary | Left | 3 | Multiplication, Division |
| `+`, `-` | Binary | Left | 2 | Addition, Subtraction |
| `==`, `!=`, `<`, `<=`, `>`, `>=` | Binary | Left | 1 | Comparisons (return 1 or 0) |
| `()` | Grouping | - | - | Parentheses for grouping |
**Arithmetic**: `+`, `-`, `*`, `/`, `^` (power), `!` (factorial), unary `-`
**Comparison**: `==`, `!=`, `<`, `<=`, `>`, `>=` (returns 1 or 0)
**Grouping**: `(` `)`

## Command Line Usage

Expand Down
4 changes: 2 additions & 2 deletions bin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "expr-solver-bin"
version = "1.0.3"
version = "1.1.0"
edition = "2024"
authors = ["Albert Varaksin <albeva@me.com>"]
description = "Binary using the expr-solver-lib to solve math expressions from command line"
Expand All @@ -15,6 +15,6 @@ name = "expr-solver"
path = "src/main.rs"

[dependencies]
expr-solver-lib = { version = "1.0.3", path = "../lib" }
expr-solver-lib = { version = "1.1.0", path = "../lib" }
clap = { version = "4.0", features = ["derive"] }
rust_decimal = { workspace = true }
44 changes: 22 additions & 22 deletions bin/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use clap::{ArgAction, Parser};
use expr_solver::{Eval, SymTable, Symbol};
use expr_solver::{SymTable, Symbol, eval_file_with_table, load_with_table};
use rust_decimal::prelude::*;
use std::path::PathBuf;

Expand Down Expand Up @@ -44,11 +44,8 @@ fn parse_key_val(s: &str) -> Result<(String, f64), Box<dyn std::error::Error + S
}

fn main() {
match run() {
Err(err) => {
eprintln!("{err}");
}
_ => {}
if let Err(err) = run() {
eprintln!("{err}");
}
}

Expand All @@ -65,25 +62,28 @@ fn run() -> Result<(), String> {
}

// load either from string input or a file
let mut eval = if let Some(expr) = args.expression.as_ref().or(args.expr.as_ref()) {
Eval::with_table(expr, table)
} else if let Some(input) = &args.input {
Eval::from_file_with_table(input.clone(), table)
} else {
return Err("no input".to_string());
};
if let Some(expr) = args.expression.as_ref().or(args.expr.as_ref()) {
let program = load_with_table(expr, table)?;

if args.assembly {
print!("{}", eval.get_assembly()?);
return Ok(());
}
if args.assembly {
print!("{}", program.get_assembly());
return Ok(());
}

// save to a file?
if let Some(output_path) = &args.output {
eval.compile_to_file(output_path)?
} else {
let res = eval.run()?;
// save to a file?
if let Some(output_path) = &args.output {
program
.save_bytecode_to_file(output_path)
.map_err(|e| e.to_string())?
} else {
let res = program.execute().map_err(|e| e.to_string())?;
println!("{res}");
}
} else if let Some(input) = &args.input {
let res = eval_file_with_table(input.to_string_lossy().as_ref(), table)?;
println!("{res}");
} else {
return Err("no input".to_string());
}

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "expr-solver-lib"
version = "1.0.3"
version = "1.1.0"
edition = "2024"
authors = ["Albert Varaksin <albeva@me.com>"]
description = "A simple math expression solver library"
Expand Down
Loading