Skip to content

Commit 10f7014

Browse files
committed
Merge branch 'release/1.20'
2 parents 637ec54 + 7349f43 commit 10f7014

24 files changed

Lines changed: 2923 additions & 1500 deletions

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ members = ["lib", "bin"]
33
resolver = "2"
44

55
[workspace.dependencies]
6-
rust_decimal = { version = "1.39", features = ["serde-bincode", "maths", "macros"] }
6+
rust_decimal = { version = "1.39", features = ["serde-bincode", "maths"] }

README.md

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,24 @@ A mathematical expression evaluator library written in Rust with support for cus
77
## Features
88

99
- **Mathematical expressions** - Arithmetic, comparisons, and built-in functions
10-
- **128-bit decimal precision** - No floating-point errors using `rust_decimal`
10+
- **Flexible numeric types** - Choose between fast f64 (default) or high-precision 128-bit Decimal
1111
- **Custom symbols** - Register your own constants and functions
1212
- **Rich error messages** - Syntax errors with source location highlighting
1313
- **Bytecode compilation** - Compile expressions to portable binary format
1414
- **Stack-based VM** - Efficient execution on a virtual machine
1515

16+
## What's it For?
17+
18+
Need to evaluate math but don't want to embed an entire scripting language? You're in the right place. 🎯
19+
20+
**Perfect for:**
21+
- **Game engines** - Calculate damage formulas, level requirements, or item stats without hardcoding values
22+
- **Configuration files** - Let users write `price * 0.9` instead of forcing them to update every value manually
23+
- **Analytics dashboards** - Enable power users to define custom metrics: `(revenue - cost) / users`
24+
- **Form calculators** - Mortgage calculators, unit converters, or any user-facing math
25+
- **Educational tools** - Math tutoring apps, graphing calculators, or homework helpers
26+
- **Learning compilers** - Clean example of lexer, parser, and stack-based VM in ~2K lines of Rust
27+
1628
## How It Works
1729

1830
Classic compiler pipeline with type-safe state transitions:
@@ -33,7 +45,35 @@ Add this to your `Cargo.toml`:
3345

3446
```toml
3547
[dependencies]
36-
expr-solver-lib = "1.1.1"
48+
expr-solver-lib = "1.2.0"
49+
```
50+
51+
**Numeric Type Selection:**
52+
53+
The library supports two numeric backends via feature flags (mutually exclusive):
54+
55+
- **`f64-floats`** (default) - Standard f64 floating-point arithmetic. Faster and simpler, allows Inf and NaN results.
56+
- **`decimal-precision`** - 128-bit Decimal for high precision. No floating-point errors, checked arithmetic with overflow detection.
57+
58+
To use high-precision Decimal:
59+
60+
```toml
61+
[dependencies]
62+
expr-solver-lib = { version = "1.2.0", default-features = false, features = ["decimal-precision"] }
63+
```
64+
65+
To enable bytecode serialization with f64:
66+
67+
```toml
68+
[dependencies]
69+
expr-solver-lib = { version = "1.2.0", features = ["serialization"] }
70+
```
71+
72+
To enable bytecode serialization with Decimal:
73+
74+
```toml
75+
[dependencies]
76+
expr-solver-lib = { version = "1.2.0", default-features = false, features = ["decimal-precision", "serialization"] }
3777
```
3878

3979
### As a binary
@@ -42,7 +82,7 @@ Add this to your `Cargo.toml`:
4282

4383
```toml
4484
[dependencies]
45-
expr-solver-bin = "1.1.1"
85+
expr-solver-bin = "1.2.0"
4686
```
4787

4888
### Quick Evaluation
@@ -60,52 +100,83 @@ let result = eval("sqrt(16) + sin(pi/2)").unwrap();
60100

61101
### Custom Symbols
62102

103+
```rust
104+
use expr_solver::{eval_with_table, SymTable, Number, ParseNumber};
105+
106+
let mut table = SymTable::stdlib();
107+
table.add_const("x", Number::parse_number("10").unwrap()).unwrap();
108+
table.add_func("double", 1, false, |args| {
109+
Ok(args[0] * Number::parse_number("2").unwrap())
110+
}).unwrap();
111+
112+
let result = eval_with_table("double(x)", table).unwrap();
113+
assert_eq!(result.to_string(), "20");
114+
```
115+
116+
Or with f64 (default):
117+
63118
```rust
64119
use expr_solver::{eval_with_table, SymTable};
65-
use rust_decimal_macros::dec;
66120

67121
let mut table = SymTable::stdlib();
68-
table.add_const("x", dec!(10)).unwrap();
69-
table.add_func("double", 1, false, |args| Ok(args[0] * dec!(2))).unwrap();
122+
table.add_const("x", 10.0).unwrap();
123+
table.add_func("double", 1, false, |args| Ok(args[0] * 2.0)).unwrap();
70124

71125
let result = eval_with_table("double(x)", table).unwrap();
72-
assert_eq!(result, dec!(20));
126+
assert_eq!(result, 20.0);
73127
```
74128

75129
### Compile Once, Execute Many Times
76130

77131
```rust
78-
use expr_solver::{load, SymTable};
79-
use rust_decimal_macros::dec;
132+
use expr_solver::{Program, SymTable};
80133

81134
// Compile expression
82-
let program = load("x * 2 + y").unwrap();
135+
let program = Program::new_from_source("x * 2 + y").unwrap();
83136

84137
// Execute with different values
85138
let mut table = SymTable::new();
86-
table.add_const("x", dec!(10)).unwrap();
87-
table.add_const("y", dec!(5)).unwrap();
139+
table.add_const("x", 10.0).unwrap();
140+
table.add_const("y", 5.0).unwrap();
88141

89142
let linked = program.link(table).unwrap();
90-
let result = linked.execute().unwrap(); // 25
143+
let result = linked.execute().unwrap(); // 25.0
144+
assert_eq!(result, 25.0);
91145
```
92146

93-
## Precision
147+
## Numeric Precision
148+
149+
The library supports two numeric backends:
94150

95-
Uses **128-bit `Decimal`** arithmetic for exact decimal calculations without floating-point errors.
151+
### f64 (Default)
152+
- Standard IEEE 754 double-precision floating-point
153+
- Fast and efficient for most use cases
154+
- Allows `Inf` and `NaN` results (e.g., `1/0``Inf`, `sqrt(-1)``NaN`)
155+
- Minimal error checking - only prevents panics
156+
157+
### Decimal (Optional)
158+
- 128-bit fixed-point arithmetic via `rust_decimal`
159+
- Exact decimal representation (no 0.1 + 0.2 ≠ 0.3 issues)
160+
- Checked arithmetic with overflow/underflow detection
161+
- Domain validation (e.g., `sqrt(-1)` returns an error)
162+
- Ideal for financial calculations or when exact decimal precision is required
163+
164+
**Choosing the right mode:**
165+
- Use **f64** (default) for general-purpose math, scientific computing, or when performance is critical
166+
- Use **Decimal** for financial applications, accounting, or when exact decimal representation is required
96167

97168
## Built-in Functions
98169

99170
| Category | Functions |
100171
|----------------|---------------------------------------------------------------------------|
101172
| **Arithmetic** | `abs`, `sign`, `floor`, `ceil`, `round`, `trunc`, `fract`, `mod`, `clamp` |
102-
| **Trig** | `sin`, `cos`, `tan`, `asin`*, `acos`*, `atan`*, `atan2`* |
103-
| **Hyperbolic** | `sinh`*, `cosh`*, `tanh`* |
104-
| **Exp/Log** | `sqrt`, `cbrt`*, `pow`, `exp`, `exp2`*, `log`, `log2`*, `log10`, `hypot`* |
173+
| **Trig** | `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2` |
174+
| **Hyperbolic** | `sinh`, `cosh`, `tanh` |
175+
| **Exp/Log** | `sqrt`, `cbrt`, `pow`, `exp`, `exp2`, `log`, `log2`, `log10`, `hypot` |
105176
| **Variadic** | `min`, `max`, `sum`, `avg` (1+ args) |
106177
| **Special** | `if(cond, then, else)` |
107178

108-
\* *Uses f64 internally, may have minor precision differences*
179+
> **Note:** In `decimal-precision` mode, some operations (inverse trig, `pow`) use internal f64 conversion due to `rust_decimal` limitations, which may introduce small precision loss.
109180
110181
## Built-in Constants
111182

@@ -153,8 +224,14 @@ expr-solver -t
153224
Run the test suite:
154225

155226
```bash
156-
# Run all tests
227+
# Run all tests with default f64 mode
157228
cargo test
229+
230+
# Test with decimal-precision mode
231+
cargo test -p expr-solver-lib --no-default-features --features decimal-precision
232+
233+
# Test binary with f64 mode
234+
cargo test -p expr-solver-bin
158235
```
159236

160237
## License

bin/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "expr-solver-bin"
3-
version = "1.1.1"
3+
version = "1.2.0"
44
edition = "2024"
55
authors = ["Albert Varaksin <albeva@me.com>"]
66
description = "Binary using the expr-solver-lib to solve math expressions from command line"
@@ -15,6 +15,5 @@ name = "expr-solver"
1515
path = "src/main.rs"
1616

1717
[dependencies]
18-
expr-solver-lib = { version = "1.1.1", path = "../lib" }
18+
expr-solver-lib = { version = "1.2.0", path = "../lib", default-features = false, features = ["f64-floats", "serialization"] }
1919
clap = { version = "4.0", features = ["derive"] }
20-
rust_decimal = { workspace = true }

bin/src/main.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use clap::{ArgAction, Parser};
2-
use expr_solver::{Program, SymTable, Symbol};
3-
use rust_decimal::prelude::*;
2+
use expr_solver::{Number, ParseNumber, Program, SymTable, Symbol};
43
use std::path::PathBuf;
54

65
/// A mathematical expression evaluator with compilation support
@@ -25,7 +24,7 @@ struct Args {
2524

2625
/// Define constants (e.g., -D x=5.0)
2726
#[arg(short = 'D', long, value_parser = parse_key_val, action = ArgAction::Append)]
28-
define: Vec<(String, f64)>,
27+
define: Vec<(String, Number)>,
2928

3029
/// List all available functions and constants
3130
#[arg(short = 't', long, conflicts_with_all=["expression", "expr", "input", "output", "assembly"])]
@@ -37,11 +36,14 @@ struct Args {
3736
}
3837

3938
/// Parses a key=value pair for custom constant definitions.
40-
fn parse_key_val(s: &str) -> Result<(String, f64), Box<dyn std::error::Error + Send + Sync>> {
39+
fn parse_key_val(s: &str) -> Result<(String, Number), Box<dyn std::error::Error + Send + Sync>> {
4140
let pos = s
4241
.find('=')
4342
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
44-
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
43+
let key = s[..pos].parse()?;
44+
let value = Number::parse_number(&s[pos + 1..])
45+
.map_err(|e| format!("Failed to parse number: {}", e))?;
46+
Ok((key, value))
4547
}
4648

4749
fn main() {
@@ -73,7 +75,7 @@ fn run() -> Result<(), String> {
7375
};
7476

7577
// Link the program with the symbol table
76-
let program = program.link(table).map_err(|err| err.to_string())?;
78+
let mut program = program.link(table).map_err(|err| err.to_string())?;
7779

7880
// Act on the loaded program
7981
if args.assembly {
@@ -91,7 +93,7 @@ fn run() -> Result<(), String> {
9193
}
9294

9395
/// Creates a symbol table with standard library and user-defined constants.
94-
fn create_symbol_table(defines: &[(String, f64)]) -> Result<SymTable, String> {
96+
fn create_symbol_table(defines: &[(String, Number)]) -> Result<SymTable, String> {
9597
let mut table = SymTable::stdlib();
9698

9799
for (name, value) in defines {
@@ -104,10 +106,7 @@ fn create_symbol_table(defines: &[(String, f64)]) -> Result<SymTable, String> {
104106
}
105107

106108
table
107-
.add_const(
108-
name.clone(),
109-
Decimal::from_f64(*value).unwrap_or(Decimal::ZERO),
110-
)
109+
.add_const(name.clone(), *value, false)
111110
.map_err(|e| format!("Failed to add constant '{}': {}", name, e))?;
112111
}
113112

lib/Cargo.toml

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[package]
22
name = "expr-solver-lib"
3-
version = "1.1.1"
3+
version = "1.2.0"
44
edition = "2024"
55
authors = ["Albert Varaksin <albeva@me.com>"]
6-
description = "A simple math expression solver library"
6+
description = "Mathematical expression evaluator with bytecode compilation and configurable numeric precision (f64 or 128-bit Decimal)"
77
license = "MIT"
88
readme = "../README.md"
99
repository = "https://github.com/albeva/expr-solver"
@@ -13,12 +13,20 @@ categories = ["mathematics", "parser-implementations"]
1313
[lib]
1414
name = "expr_solver"
1515

16+
[features]
17+
default = ["f64-floats"]
18+
f64-floats = []
19+
decimal-precision = ["dep:rust_decimal", "dep:rust_decimal_macros"]
20+
serialization = ["dep:serde", "dep:bincode"]
21+
1622
[dependencies]
17-
indoc = "2.0.6"
18-
bincode = { version = "2.0.1", features = ["serde"] }
19-
serde = { version = "1.0", features = ["derive"] }
23+
bincode = { version = "2.0.1", features = ["serde"], optional = true }
24+
serde = { version = "1.0", features = ["derive"], optional = true }
2025
colored = "3.0.0"
2126
thiserror = "2.0.17"
2227
unicode-width = "0.2.2"
23-
rust_decimal_macros = { version = "1.39" }
24-
rust_decimal = { workspace = true }
28+
rust_decimal_macros = { version = "1.39", optional = true }
29+
rust_decimal = { workspace = true, optional = true }
30+
31+
[dev-dependencies]
32+
indoc = "2.0.6"

0 commit comments

Comments
 (0)