Metadata-Version: 2.4
Name: py_matheval
Version: 0.2.3
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
License-File: LICENSE
Summary: Math expression evaluator with exact Decimal arithmetic, gas metering and nesting-depth limits — powered by Rust
Author-email: kiortir <kiortir@yandex.ru>
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/kiortir/py-matheval
Project-URL: Repository, https://github.com/kiortir/py-matheval

# py_matheval

Math expression evaluator for Python with exact `Decimal` arithmetic, gas
metering and nesting-depth limits — powered by Rust.

## Features

- **Exact arithmetic** — every number is a `decimal.Decimal`; no binary
  floating-point surprises (`0.1 + 0.2 == 0.3`).
- **Gas metering** — each evaluation runs under a configurable gas budget;
  runaway computations raise `GasExhaustedError` instead of hanging.
- **Nesting-depth limit** — deeply nested expressions are rejected up front
  with a clear error (default 15, configurable up to 1024) instead of
  crashing the interpreter.
- **Compile once, evaluate many times** — expressions compile to bytecode
  for a stack VM; `evaluate()` reuses the compiled form.
- **Variables** — named variables (`a`, `price_2`) and UUID-keyed variables
  (`{550e8400-e29b-41d4-a716-446655440000}`), both passable per call.
- **Serialization** — pickle round-trips, eval-style `repr()`, equality by
  bytecode, and bytecode JSON export/import.
- **GIL released** during evaluation, so CPU-bound expressions from multiple
  Python threads run in parallel.

## Installation

Python 3.10+.

From PyPI:

```bash
pip install py_matheval
```

From source:

```bash
git clone https://github.com/kiortir/py-matheval
cd py-matheval
pip install maturin
maturin develop --release
```

## Quickstart

One-off evaluation via the module-level shortcut:

```python
from py_matheval import evaluate

evaluate("1 + 2 * 3")                    # Decimal('7')
evaluate("2 ^ 10")                       # Decimal('1024')
evaluate("a + b", vars={"a": 2, "b": 3}) # Decimal('5')
```

Compile once, evaluate repeatedly with a gas budget:

```python
from py_matheval import CompiledExpression, GasExhaustedError

expr = CompiledExpression("a * 2 + b", max_depth=8)
expr.variables()                          # ['a', 'b']
expr.evaluate({"a": 5, "b": 1})           # Decimal('11')

try:
    expr.evaluate({"a": 5, "b": 1}, gas=1)
except GasExhaustedError:
    print("out of gas")                   # out of gas
```

Serialization:

```python
import pickle
from py_matheval import CompiledExpression

expr = CompiledExpression("min(a, 10) * 2")
repr(expr)                                # "CompiledExpression('min(a, 10) * 2')"
pickle.loads(pickle.dumps(expr)) == expr  # True
expr.to_bytecode_json()                   # JSON array of bytecode ops
```

## API overview

```python
evaluate(expression, vars=None, gas=None, max_depth=None) -> Decimal

class CompiledExpression:
    CompiledExpression(expression, max_depth=None)
    evaluate(vars=None, gas=None) -> Decimal
    variables() -> list[str]
    to_bytecode_json() -> str
    @classmethod
    from_bytecode_json(json: str) -> CompiledExpression

exception GasExhaustedError(RuntimeError)
```

### Operators

| Operator | Meaning | Notes |
|---|---|---|
| `+` `-` `*` | add, subtract, multiply | |
| `/` | division | `Decimal` division |
| `//` `%` | floor division, modulo | |
| `^` | power | right-associative |
| `-x` `+x` | unary minus, plus | |
| `==` `!=` `<` `<=` `>` `>=` | comparisons | |
| `not` `and` `or` | logic | short-circuit, lowest precedence |
| `c ? t : f` | ternary | lowest precedence |

Comparison and logic operators return `Decimal` `1`/`0`.

### Functions

`min`, `max`, `sqrt`, `abs`, `floor`, `ceil`, `sum`, `average`, `add`,
`mul`, `round` — all variadic where it makes sense, e.g.
`min(3, 1, 2)`, `sum(1, 2, 3)`, `average(2, 4)`.

### Errors

| Error | Raised when |
|---|---|
| `ValueError` | parse error, nesting depth exceeded, invalid `max_depth`/`gas` |
| `RuntimeError` | unknown variable (`Variable not found: name`) |
| `GasExhaustedError` | gas budget exhausted (subclass of `RuntimeError`) |
| `OverflowError` | `Decimal` overflow |
| `ZeroDivisionError` | division or modulo by zero |

## Safety

### Gas metering

Every operation has a gas cost; evaluation aborts with `GasExhaustedError`
once the budget is spent:

| Operation | Cost |
|---|---|
| Push const, load var, jumps, comparisons, logic, `not`, `neg` | 1 |
| `+` `-` `*` `/` `//` `%` and function calls | 3 |
| `^` | `1 + ⌈|exponent|⌉` |

`gas` is a non-negative integer; `None` means no limit.

### Nesting depth

`max_depth` limits how deeply an expression may nest (number of nodes on
the longest path, root = 1). Default `15`, hard cap `1024`; values outside
`1..=1024` raise `ValueError`. `max_depth=None` means the default.
Over-deep expressions raise
`ValueError: Parse error: expression nesting depth N exceeds max_depth M`
at construction time — before any evaluation.

## Local development

```bash
# Rust tests
cargo test

# Build the extension into .venv (needs cargo and uv on PATH)
PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH" \
    .venv/bin/maturin develop --uv

# Python tests
.venv/bin/pytest

# Regenerate type stubs py_matheval/_lib/__init__.pyi
cargo run --bin stub_gen --features stub-gen
```

`cargo` lives in `~/.cargo/bin` and `uv` in `~/.local/bin` — extend `PATH`
as shown above before calling `maturin`.

## License

MIT — see [LICENSE](LICENSE).

