expr_solver/
lib.rs

1//! A mathematical expression evaluator library with bytecode compilation.
2//!
3//! This library provides a complete compiler pipeline for mathematical expressions,
4//! from parsing to bytecode execution on a stack-based virtual machine.
5//!
6//! # Features
7//!
8//! - **Flexible numeric types** - Choose between f64 (default) or 128-bit Decimal precision
9//! - **Rich error messages** - Parse errors with syntax highlighting
10//! - **Bytecode compilation** - Compile once, execute many times
11//! - **Custom symbols** - Add your own constants and functions
12//! - **Local constants** - `let` ... `then` syntax for declaring scoped constants
13//! - **Control flow** - Conditional expressions with `if(condition, then, else)`
14//! - **Serialization** - Save/load compiled programs (requires `serialization` feature)
15//!
16//! ## Numeric Type Selection
17//!
18//! The library supports two numeric backends via feature flags:
19//!
20//! - **`f64-floats`** (default) - Standard f64 floating-point arithmetic. Faster and simpler,
21//!   suitable for most use cases. Allows Inf and NaN results.
22//! - **`decimal-precision`** - 128-bit Decimal arithmetic for high precision. No floating-point
23//!   errors, checked arithmetic with overflow detection. Use for financial calculations or when
24//!   exact decimal representation is required.
25//!
26//! **Note**: Only one numeric backend can be enabled at a time.
27//!
28//! # Quick Start
29//!
30//! ```
31//! use expr_solver::{eval, num};
32//!
33//! let result = eval("2 + 3 * 4").unwrap();
34//! assert_eq!(result, num!(14));
35//! ```
36//!
37//! ## Working with Numbers
38//!
39//! Use the [`num!`] macro to create numeric values that work with both f64 and Decimal backends:
40//!
41//! ```
42//! use expr_solver::num;
43//!
44//! let x = num!(42);      // Works with any backend
45//! let y = num!(3.14);    // Supports decimals
46//! let z = num!(-10);     // And negative numbers
47//! ```
48//!
49//! The macro ensures your code works regardless of which numeric backend is enabled.
50//!
51//! # Custom Symbols
52//!
53//! ```
54//! use expr_solver::{num, eval_with_table, SymTable};
55//!
56//! let mut table = SymTable::stdlib();
57//! table.add_const("x", num!(10), false).unwrap();
58//!
59//! let result = eval_with_table("x * 2", table).unwrap();
60//! assert_eq!(result, num!(20));
61//! ```
62//!
63//! # Advanced: Type-State Pattern
64//!
65//! The `Program` type uses the type-state pattern to enforce correct usage:
66//!
67//! ```
68//! use expr_solver::{num, SymTable, Program};
69//!
70//! // Compile expression to bytecode
71//! let program = Program::new_from_source("x + y").unwrap();
72//!
73//! // Link with symbol table (validated at link time)
74//! let mut table = SymTable::new();
75//! table.add_const("x", num!(10), false).unwrap();
76//! table.add_const("y", num!(5), false).unwrap();
77//!
78//! let mut linked = program.link(table).unwrap();
79//!
80//! // Execute
81//! let result = linked.execute().unwrap();
82//! assert_eq!(result, num!(15));
83//! ```
84//!
85//! # Local Constants with LET THEN
86//!
87//! Declare local constants using `let` ... `then` syntax:
88//!
89//! ```
90//! use expr_solver::{eval, num};
91//!
92//! // Single declaration
93//! let result = eval("let x = 10 then x * 2").unwrap();
94//! assert_eq!(result, num!(20));
95//!
96//! // Multiple declarations
97//! let result = eval("let x = 5, y = 3 then x + y").unwrap();
98//! assert_eq!(result, num!(8));
99//!
100//! // Reference previous declarations
101//! let result = eval("let x = 2, y = x * 3, z = y + 1 then z").unwrap();
102//! assert_eq!(result, num!(7));
103//!
104//! // Use with globals and functions
105//! let result = eval("let r = 5, area = pi * r ^ 2 then area").unwrap();
106//! assert!((result - num!(78.53981633974483)).abs() < num!(0.0001));
107//! ```
108//!
109//! **Notes:**
110//! - Local constants are evaluated left-to-right
111//! - Can reference previously declared locals and globals
112//! - Cannot reference forward (e.g., `let x = y, y = 1 then x` is an error)
113//! - Cannot shadow global constants (e.g., `let pi = 3 then pi` is an error)
114//! - Duplicate names in the same `let` block are errors
115//!
116//! # Supported Operators
117//!
118//! - Arithmetic: `+`, `-`, `*`, `/`, `^` (power), `!` (factorial), unary `-`
119//! - Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=` (return 1 or 0)
120//! - Grouping: `(` `)`
121//! - Control flow: `if(condition, then_value, else_value)`
122//!
123//! # Built-in Functions
124//!
125//! See [`SymTable::stdlib()`] for the complete list of built-in functions and constants.
126//!
127//! # Architecture
128//!
129//! The library uses a multi-stage compilation pipeline:
130//!
131//! 1. **Parsing** ([`Parser`]) - Source code → Abstract Syntax Tree (AST)
132//! 2. **IR Generation** ([`IrBuilder`]) - AST → Bytecode + Symbol metadata
133//! 3. **Linking** ([`Linker`]) - Bytecode + SymTable → Linked bytecode
134//! 4. **Execution** ([`Vm`]) - Linked bytecode → Result
135//!
136//! Each stage has its own error type ([`ParseError`], [`IrError`], [`LinkerError`], [`VmError`])
137//! which automatically converts to [`ProgramError`] for convenience.
138
139// Core types (shared)
140mod ir;
141mod number;
142mod span;
143mod symbol;
144mod symtable;
145mod token;
146mod vm;
147
148// Expression solver implementation
149mod ast;
150mod error;
151mod ir_builder;
152mod lexer;
153mod linker;
154mod metadata;
155mod parser;
156mod program;
157
158// Public API
159pub use ast::{BinOp, Expr, ExprKind, UnOp};
160pub use error::{IrError, LinkerError, ParseError, ProgramError};
161pub use metadata::{SymbolKind, SymbolMetadata};
162pub use number::{Number, ParseNumber};
163pub use parser::Parser;
164pub use program::{Compiled, Linked, Program, ProgramOrigin};
165pub use symbol::{Symbol, SymbolError};
166pub use symtable::SymTable;
167pub use vm::{Vm, VmError};
168
169// ============================================================================
170// Helper functions for evaluating expressions
171// ============================================================================
172
173/// Evaluates an expression string with the standard library.
174///
175/// # Examples
176///
177/// ```
178/// use expr_solver::{eval, num};
179///
180/// let result = eval("2 + 3 * 4").unwrap();
181/// assert_eq!(result, num!(14));
182/// ```
183pub fn eval(expression: &str) -> Result<Number, String> {
184    eval_with_table(expression, SymTable::stdlib())
185}
186
187/// Evaluates an expression string with a custom symbol table.
188///
189/// # Examples
190///
191/// ```
192/// use expr_solver::{num, eval_with_table, SymTable};
193///
194/// let mut table = SymTable::stdlib();
195/// table.add_const("x", num!(42), false).unwrap();
196///
197/// let result = eval_with_table("x * 2", table).unwrap();
198/// assert_eq!(result, num!(84));
199/// ```
200pub fn eval_with_table(expression: &str, table: SymTable) -> Result<Number, String> {
201    Program::new_from_source(expression)
202        .map_err(|err| err.to_string())?
203        .link(table)
204        .map_err(|err| err.to_string())?
205        .execute()
206        .map_err(|err| err.to_string())
207}
208
209/// Evaluates an expression from a binary file with the standard library.
210///
211/// # Examples
212///
213/// ```no_run
214/// use expr_solver::eval_file;
215///
216/// let result = eval_file("expr.bin").unwrap();
217/// ```
218#[cfg(feature = "serialization")]
219pub fn eval_file(path: impl AsRef<str>) -> Result<Number, String> {
220    eval_file_with_table(path, SymTable::stdlib())
221}
222
223/// Evaluates an expression from a binary file with a custom symbol table.
224///
225/// # Examples
226///
227/// ```no_run
228/// use expr_solver::{eval_file_with_table, SymTable};
229///
230/// let result = eval_file_with_table("expr.bin", SymTable::stdlib()).unwrap();
231/// ```
232#[cfg(feature = "serialization")]
233pub fn eval_file_with_table(path: impl AsRef<str>, table: SymTable) -> Result<Number, String> {
234    let program = Program::new_from_file(path.as_ref()).map_err(|err| err.to_string())?;
235    let mut linked = program.link(table).map_err(|err| err.to_string())?;
236    linked.execute().map_err(|err| err.to_string())
237}