formulac 0.8.0

A complex-number and extensible function supported math expression parser for Rust
Documentation

formulac

formulac is a Rust library for parsing, evaluating, and differentiating mathematical expressions. It supports complex numbers, user-defined functions, higher-order derivatives, and generic numeric types. Ideal for symbolic computation, mathematical simulations, and evaluating formulas in Rust applications.

Features

  • Generic numeric backend (T: Real)
    • Arbitrary-precision support via Real trait
    • Supports f64 as well as custom numeric types (e.g., arbitrary precision floats)
    • Designed without requiring Copy, enabling efficient use of non-trivial numeric types
  • Complex number support
    • Built on num_complex::Complex<T>.
  • Const-generic API
    • Function argument arity is encoded in the type system (Builder<T, N>)
    • Eliminates runtime argument length checks and enables compile-time optimization
  • Reverse Polish Notation (RPN)
    • Converts infix expressions to RPN using the Shunting-Yard algorithm
  • Built-in mathematical operators & functions
    • Supports +, -, *, /, ^, and standard functions like sin, cos, exp, log, and more
    • See src/astnode.rs or API Overview for the list of available functions, constants, and operator symbols
  • Abstract Syntax Tree (AST)
    • Expressions are parsed into AstNode structures, enabling inspection, simplification, and compilation into executable closures
  • User-defined functions
    • Easily register custom functions via Builder::with_user_functions
  • Symbolic differentiation
    • Supports diff(...) operator
    • Higher-order derivatives supported
  • Safe and dependency-light
    • No unsafe
    • Minimal dependencies

Usage

Add to your Cargo.toml:

cargo add formulac

Basic Example

use num_complex::Complex;
use formulac::Builder;

fn main() {
    // 1 argument: z for T = f64
    let expr = Builder::<f64, 1>::new("sin(z) + a * cos(z)", ["z"])
        .with_constants([("a", Complex::new(3.0, 2.0))])
        .compile()
        .unwrap();

    let result = expr([Complex::new(1.0, 2.0)]);
    println!("Result = {}", result);
}

Using Custom Numeric Types

formulac is generic over numeric type T.

Builder::<f64, 1>::new(...)

can be replaced with other types implementing Real.

Requirements for T

T must implement the crate-defined Real trait, which is used throughout the library as the underlying numeric type for Complex<T>.

It provides:

  • Basic arithmetic operations
  • Conversion from primitive numeric types
  • Mathematical constants and functions

This design allows integration with:

  • arbitrary precision floats
  • domain-specific numeric types

Note:

  • T is not required to implement Copy
  • Values are handled via Clone internally

For full details, see crate::core::Real.

Registering a Custom Function

You can register custom functions using Builder::with_user_functions.

use num_complex::Complex;
use formulac::{Builder, UserFn};

fn main() {
    // Define a function f(x) = x^2 + 1
    let func = UserFn::<f64>::new("f", |[x]| x * x + Complex::new(1.0, 0.0));

    let builder = Builder::<f64, 1>::new("f(3)", [])
        .with_user_functions([func]);

    let expr = builder.compile()
        .expect("Failed to compile formula with UserFn");

    assert_eq!(expr([]), Complex::new(10.0, 0.0));

    let func2 = UserFn::<f64>::new(
        "f", // it conflicts the above function.
        |[x]| x + Complex::new(2.0, 1.0),
    );

    // If multiple functions with the same name are provided, the later one overrides the former.
    let expr = builder.with_user_functions([func2])
        .compile().unwrap();

    assert_eq!(expr([]), Complex::new(5.0, 1.0));
}

Differentiation Support

formulac can represent derivative expressions in the AST. Built-in functions (e.g. sin, cos, exp, log, …) already have derivative rules, but user-defined functions require the user to explicitly register their derivative form. If no derivative is provided, diff(...) will result in an error at compile time (during compile()).

Note on Differential Order:

  • Only integer-order derivatives are supported; fractional derivatives are not allowed.
  • The maximum allowed order is i8::MAX (127). Attempting to compute a derivative higher than this will result in a runtime error.

Differentiation Example

You can directly write differentiation expressions using the diff operator:

use num_complex::Complex;
use formulac::{Builder, UserFn};

fn main() {
    // Differentiate sin(x) with respect to x
    let formula = "diff(sin(x), x)";
    let expr = Builder::<f64, 1>::new(formula, ["x"])
        .compile()
        .expect("Failed to compile formula");

    let result = expr([Complex::new(1.0, 0.0)]); // evaluates cos(1)
    println!("Result = {}", result);
}

When computing derivatives of order 2 or higher, specify the order:

use num_complex::Complex;
use formulac::{Builder, UserFn};

fn main() {
    // Differentiate sin(x) with respect to x
    let formula = "diff(sin(x), x, 2)";
    let expr = Builder::<f64, 1>::new(formula, ["x"])
        .compile()
        .expect("Failed to compile formula");

    let result = expr([Complex::new(1.0, 0.0)]); // evaluates to -sin(1)
    println!("Result = {}", result);
}

Example: User-defined function with derivative

You can define your own functions and provide derivatives for them. The derivative must be registered in the same order as the function arguments.

use num_complex::Complex;
use formulac::{Builder, UserFn};

fn main() {
    // Define f(x) = x^2, derivative f'(x) = 2x
    let deriv = UserFn::<f64>::new("df", |[x]| Complex::new(2.0, 0.0) * x);
    let func = UserFn::<f64>::new("f", |[x]| x * x).with_derivative([deriv]).unwrap();

    let expr = Builder::<f64, 1>::new("diff(f(x), x)", ["x"])
        .with_user_functions([func])
        .compile()
        .expect("Failed to compile formula with UserFn");

    let result = expr([Complex::new(3.0, 0.0)]); // evaluates f'(3) = 6
    println!("Result: {}", result);
}

Example: Multi-variable functions (Partial derivatives)

For functions with multiple variables, you can register partial derivatives with respect to each argument. Use the same order as the function arguments.

use num_complex::Complex;
use formulac::{Builder, UserFn};

fn main() {
    // Define a partial derivative w.r.t x: ∂g/∂x = 2*x*y
    let deriv_x = UserFn::<f64>::new("dg_dx", |[x, y]| Complex::new(2.0, 0.0) * x * y);
    // Define a partial derivative w.r.t y: ∂g/∂y = x^2 + 3*y^2
    let deriv_y = UserFn::<f64>::new("dg_dy", |[x, y]| x * x + Complex::new(3.0, 0.0) * y * y);
    // Define g(x, y) = x^2 * y + y^3
    let func = UserFn::<f64>::new("g", |[x, y]| x * x * y  + y * y * y)
        .with_derivative([deriv_x, deriv_y]);

    // 2 arguments: x and y
    let expr_dx = Builder::<f64, 2>::new("diff(g(x, y), x)", ["x", "y"])
        .with_user_functions([func.clone()]) // use it again later
        .compile()
        .unwrap();
    let result_dx = expr_dx([Complex::new(2.0, 0.0), Complex::new(3.0, 0.0)]);
    println!("∂g/∂x at (2, 3) = {}", result_dx); // 12

    let expr_dy = Builder::<f64, 2>::new("diff(g(x, y), y)", ["x", "y"])
        .with_user_functions([func])
        .compile()
        .unwrap();
    let result_dy = expr_dy([Complex::new(2.0, 0.0), Complex::new(3.0, 0.0)]);
    println!("∂g/∂y at (2, 3) = {}", result_dy); // 31
}

Core Types & API Overview

  • Builder<T, const N: usize> Compiles a formula string into a Rust closure Fn([Complex<T>; N]) -> Complex<T> that evaluates the expression for given variable values.

  • UserFn<T> Represents a user-defined function. Accepts a fixed-size array [Complex<T>; N] as arguments.

Notes on Design

  • T is not required to implement Copy
    • Enables support for heavy numeric types (e.g., arbitrary precision floats)
  • Values are handled via Clone
    • Cost depends on the underlying type
    • For small types (e.g., f64), this is negligible
    • For large types, this trades performance for flexibility
  • Closure require:
    T: Send + Sync + 'static
    
    • This is needed because the compiled function captures owned data
    • Enables safe reuse across threads

Available mathematical constants

String Value Description
E e The base of natural logarithm (≈ 2.71828)
FRAC_1_PI 1 / π Reciprocal of π
FRAC_1_SQRT_2 1 / √2 Reciprocal of square root of 2
FRAC_2_PI 2 / π 2 divided by π
FRAC_2_SQRT_PI 2 / √π 2 divided by square root of π
FRAC_PI_2 π / 2 Half of π
FRAC_PI_3 π / 3 One-third of π
FRAC_PI_4 π / 4 One-fourth of π
FRAC_PI_6 π / 6 One-sixth of π
FRAC_PI_8 π / 8 One-eighth of π
LN_2 ln(2) Natural logarithm of 2
LN_10 ln(10) Natural logarithm of 10
LOG2_10 log2(10) Base-2 logarithm of 10
LOG2_E log2(e) Base-2 logarithm of e
LOG10_2 log10(2) Base-10 logarithm of 2
LOG10_E log10(e) Base-10 logarithm of e
PI π Ratio of circle circumference to diameter
SQRT_2 √2 Square root of 2
TAU 2 * π Full circle in radians

Available unary operators

String Function Description
+ Positive Identity operator
- Negative Negation operator

Available binary operators

String Function Description
+ Add Addition
- Sub Subtraction
* Mul Multiplication
/ Div Division
^ Pow Power (x^y)

Available Functions

String Function Description
sin Sin(x) Sine function
cos Cos(x) Cosine function
tan Tan(x) Tangent function
asin Asin(x) Arc sine
acos Acos(x) Arc cosine
atan Atan(x) Arc tangent
sinh Sinh(x) Hyperbolic sine
cosh Cosh(x) Hyperbolic cosine
tanh Tanh(x) Hyperbolic tangent
asinh Asinh(x) Hyperbolic arcsine
acosh Acosh(x) Hyperbolic arccosine
atanh Atanh(x) Hyperbolic arctangent
exp Exp(x) Exponential function e^x
ln Ln(x) Natural logarithm
log10 Log10(x) Base-10 logarithm
sqrt Sqrt(x) Square root
abs Abs(x) Absolute value
conj Conj(x) Complex conjugate
pow Pow(x, y) x raised to y (complex exponent)
powi Powi(x, n) x raised to integer n

Available differential operator

String Function Description
diff(f, x) diff(f, x) First-order derivative of f with respect to x
diff(f, x, n) diff(f, x, n) n-th order derivative of f with respect to x (max i8::MAX)

Benchmarking

The formulac crate provides benchmarks using the Criterion crate to measure both compilation and execution performance.

Note:

  • Criterion is a dev-dependency, so benchmarks are only available in development builds.
  • :warning: Some benchmarks (e.g., 1000 nested operations) may take longer to run.
  • Benchmarks require the criterion crate as a dev-dependency and are intended for development/testing purposes only.

Benchmark Source

The benchmarks are located in benches/benches.rs and cover:

  • Linear expressions (many operands like polynomials)
  • Nested expressions (e.g., sin(sin(...)))
  • Numeric literals
  • Expressions with and without parentheses
  • Many constants references (up to 100 constants)
  • Differentiate expressions
  • Invalid expressions (error cases)
  • Practical expressions (polynomials, wave functions, exponential decay)
  • Comparison of direct calls vs. parsed calls for standard functions (sin, cos, pow, etc.)

Run Benchmarks

Run the benchmarks with:

cargo bench

Both compile and exec times are measured. Criterion generates detailed statistics and plots in target/criterion.

Viewing Results

Open the generated HTML report in a browser to view benchmark results and comparisons:

xdg-open target/criterion/report/index.html

License

Licensed under MIT OR Apache-2.0 — choose the license that best suits your project.


Contribution & Contact

Contributions, feature requests, and bug reports are welcome! Please feel free to open issues or submit pull requests via the GitHub repository.