Skip to main content

mathjs-to-func

PyPI Ruff Black ty CI Ask DeepWiki

A tiny Python library that compiles serialized math.js expression trees into fast, reusable Python callables. The generated function respects dependency ordering, validates inputs, and mirrors a practical subset of math.js operators, constants, comparisons, conditionals, and numeric functions.

Key Features

  • Execute without reparsing or repeatedly walking the JSON graph.
  • Detect dependency cycles and missing identifiers early.
  • Keep execution sandboxed by compiling a controlled Python AST.
  • Work well with scalars or NumPy arrays for vectorised workloads.
  • Resolve common math.js constants like pi, e, tau, NaN, and Infinity.

Installation

The project uses uv for dependency and virtualenv management. From the repository root:

uv add mathjs-to-func

An optional parse extra installs a JSON-to-math.js parser powered by Pydantic:

uv add mathjs-to-func --extra parse

Compiling A Function

from mathjs_to_func import build_evaluator, to_dot, to_string

def main():
    mathjs_payload = {
        "expressions": {
            # z = (x + y) / 2
            "sum_xy": {
                "type": "OperatorNode",
                "fn": "add",
                "args": [
                    {"type": "SymbolNode", "name": "x"},
                    {"type": "SymbolNode", "name": "y"},
                ],
            },
            "mean": {
                "type": "OperatorNode",
                "fn": "divide",
                "args": [
                    {"type": "SymbolNode", "name": "sum_xy"},
                    {"type": "ConstantNode", "value": "2", "valueType": "number"},
                ],
            },
        },
        "inputs": ["x", "y"],
        "target": "mean",
    }

    evaluator = build_evaluator(**mathjs_payload, include_source=True)

    result = evaluator({"x": 10, "y": 6})
    print(result)  # -> 8.0

    # Introspection helpers
    print(evaluator.__mathjs_required_inputs__)     # ('x', 'y')
    print(evaluator.__mathjs_evaluation_order__)    # ('sum_xy', 'mean')
    print(evaluator.__mathjs_inputs_referenced_per_target__)  # {'mean': ('x', 'y')}
    print(evaluator.__mathjs_source__)              # Generated Python source
    print(to_string(mathjs_payload))                 # sum_xy / 2
    print(to_dot(mathjs_payload))                    # Dependency graph

Parameters

build_evaluator accepts keyword parameters (or a single payload mapping containing the same keys):

Argument Type Description
expressions Mapping[str, Mapping[str, Any]] math.js AST JSON keyed by expression id. Each id becomes a local variable in the compiled function.
inputs Iterable[str] Whitelisted identifiers that may be supplied when the function is invoked.
target str | Sequence[str] Name of the expression to return, or multiple expression names to return as a dict[str, Any].
config EvalConfig | Mapping[str, object] (optional) Per-evaluator runtime options. rel_tol, abs_tol, and math.js-style epsilon control comparison tolerances; comparison selects "mathjs", "numpy", or "strict" equality semantics; result_dtype selects "auto", "numpy", or "python" scalar demotion policy.
compile_cache bool (optional) Reuse compiled functions through an opt-in structural LRU cache.
compile_cache_maxsize int | None (optional) Cache size when compile_cache=True; defaults to 128, and None makes the cache unbounded.
include_source bool (optional) Attach executable generated Python source code as __mathjs_source__ on the returned callable.

The returned callable always expects a single mapping argument with the provided inputs. It returns the evaluated target value when target is a string, or a dict[str, Any] in the requested target order when target is a sequence, and may be reused across invocations.

When target is a sequence, the callable returns a mapping in the requested target order:

exprs = {
    "low": {
        "type": "OperatorNode",
        "fn": "subtract",
        "args": [
            {"type": "SymbolNode", "name": "x"},
            {"type": "ConstantNode", "value": "2", "valueType": "number"},
        ],
    },
    "high": {
        "type": "OperatorNode",
        "fn": "add",
        "args": [
            {"type": "SymbolNode", "name": "x"},
            {"type": "ConstantNode", "value": "2", "valueType": "number"},
        ],
    },
}

evaluator = build_evaluator(
    expressions=exprs,
    inputs=["x"],
    target=["low", "high"],
    include_source=True,
)
assert evaluator({"x": 10}) == {"low": 8, "high": 12}

The source string includes the import preamble needed to re-execute it in an environment where mathjs_to_func is installed:

namespace = {}
exec(evaluator.__mathjs_source__, namespace)
assert namespace["_compiled"]({"x": 10}) == evaluator({"x": 10})

Supported math.js nodes

Node Notes
ConstantNode numeric (number), string, boolean, or null literals
SymbolNode inputs, expression references, and common built-in constants; identifiers must be alphanumeric/underscore, starting with a letter/underscore
OperatorNode add, subtract, multiply, divide, pow, mod, unary unaryPlus, unaryMinus, not, and, or, xor, comparisons, and nullish
FunctionNode Common math.js numeric/statistical helpers, including trig, logs, format, clamp, hypot, integer combinatorics, variance, std, mode, ifnull, and operator aliases such as add(a, b)
ParenthesisNode forwards to the wrapped expression
ArrayNode materialised to Python lists/NumPy arrays
AccessorNode/IndexNode read-only indexing with math.js 1-based indices translated to Python 0-based indices
RangeNode materialised to inclusive NumPy ranges with optional non-zero step
ObjectNode materialised to Python dict literals with string keys
ConditionalNode lazy scalar ternary evaluation, vectorised NumPy where for arrays
RelationalNode chained comparisons like 10 < x <= 50, with scalar short-circuiting

Unknown node types, invalid identifiers, or disallowed functions raise InvalidNodeError during compilation.

See docs/compatibility.md for the fuller math.js compatibility matrix and known gaps.

Error handling

  • ExpressionError: base class for configuration mistakes.
  • MissingTargetError: requested target id does not exist.
  • UnknownIdentifierError: an expression references a symbol that is neither an input nor another expression.
  • CircularDependencyError: dependency graph contains a cycle.
  • InvalidNodeError: AST contains unsupported structures or invalid literals.
  • InputValidationError: the compiled function received inputs that are missing, unexpected, or not a mapping.
  • RuntimeEvaluationError: a compiled expression failed at runtime; the original exception is preserved as __cause__.

All exceptions provide enough context (expression name, offending identifier, cycle list, etc.) to surface descriptive UI errors.

Parsing math.js JSON

With the extra installed you can turn serialized math.js nodes into evaluator-ready mappings:

from mathjs_to_func import build_evaluator
from mathjs_to_func.parse import parse

expression = parse(
    """{
    "type": "OperatorNode",
    "fn": "add",
    "args": [
        {"type": "SymbolNode", "name": "x"},
        {"type": "ConstantNode", "value": "2", "valueType": "number"}
    ]
}"""
)

evaluator = build_evaluator(
    expressions={"total": expression},
    inputs=["x"],
    target="total",
)

result = evaluator({"x": 40})  # -> 42

For complete {expressions, inputs, target} envelopes, use parse_payload:

from mathjs_to_func import build_evaluator
from mathjs_to_func.parse import parse_payload

payload = parse_payload(serialized_payload)
evaluator = build_evaluator(payload=payload)

The parser also exposes Pydantic models such as ConstantNode, SymbolNode, and OperatorNode for typed payload construction. math.js replacer values such as Complex, Unit, BigNumber, and Fraction are rejected with explicit unsupported-value errors until those runtime types are implemented.

All examples below assume commands are wrapped with uv run ... to execute inside the managed environment.

CLI

Compile a payload file and inspect the generated Python source without writing a script:

uv run python -m mathjs_to_func compile payload.json --target z --emit-source

Without --emit-source, the command validates the payload and prints metadata JSON containing the target, required inputs, and evaluation order. Use - as the payload path to read JSON from stdin.

JSON Schema

Export JSON Schema for frontend validation of serialized math.js payloads:

uv run python -m mathjs_to_func schema --output dist/mathjs-to-func.schema.json

The default schema covers a complete evaluator payload (expressions, inputs, and target). Use --kind expression to export the schema for a single math.js expression tree.

Implementation Notes

  1. AST translation – MathJsAstBuilder walks the math.js JSON and emits Python ast.AST nodes. Identifiers are validated via a strict regex, and the generated runtime reserves the __mj_ prefix for internal names.
  2. Dependency graph – A topological sorter (graphlib.TopologicalSorter) runs over expression references to produce a safe evaluation order while catching cycles and missing references upfront.
  3. Code generation – The generated function validates the provided scope, binds required inputs to local variables, evaluates expressions in order, and returns the target. Intermediate values are stored as local variables named after their expression id.
  4. Execution sandbox – The compiled module is executed with a tightly scoped globals dictionary: helper math functions and a few safe built-ins only. There is no ambient __builtins__ exposure.
  5. Helper functions – math.js functions map onto small Python helpers for arithmetic, comparison, logical, nullish, formatting, and statistics behavior. Equality and ordering default to math.js-style tolerances for numeric round-off, configurable per evaluator with EvalConfig, {"epsilon": ...}, or {"comparison": "mathjs" | "numpy" | "strict"} for explicit comparison semantics.

Introspection

Compiled evaluators expose __mathjs_inputs_referenced_per_target__ alongside the existing source, target, input, and evaluation-order metadata. The public helpers to_string(payload), to_tex(payload), to_dot(payload), to_mermaid(payload), and inputs_referenced_per_target(payload) can be used before compilation to build UI labels, LaTeX tooltips, and dependency graph views.

Cache Notes

compile_cache=True now uses a structural key rather than serializing through JSON. This avoids JSON round-trips for deep trees and lets hashable non-JSON constants participate in cache keys. The cache remains opt-in and process-local; tune it with compile_cache_maxsize.

Testing

Run the full suite with:

uv run pytest

Run the benchmark suite locally with:

npm ci --prefix bench/js
uv run python -m bench

CI runs uv run python -m bench --check as a relative perf regression gate. The benchmark compares reusable build_evaluator call performance with Python eval, simpleeval, and a Node math.js parse → JSON round-trip → compile path across scalar arithmetic, conditional, helper-heavy, and NumPy payloads.

Run mutation testing with:

uv run mutmut run
uv run mutmut results

The GitHub mutation workflow runs on source and test changes, records the full mutmut result set, and emits a warning when any mutants survive.

The tests cover operator translation, helper semantics, dependency validation, error conditions, numpy-friendly behaviour, and public API ergonomics.

Project Structure

src/mathjs_to_func/
├── __init__.py          # build_evaluator public API and export list
├── ast_builder.py       # math.js JSON → Python AST translation
├── compiler.py          # dependency graph, code generation, compilation
├── errors.py            # structured exception hierarchy
├── helpers.py           # runtime helpers for math.js-compatible functions/operators
└── py.typed             # PEP 561 marker for type-aware consumers

Additional documentation lives in docs/api_design.md, outlining the initial design considerations.

Limitations & Future Work

  • Only a subset of math.js functions/operators are implemented today; see the compatibility matrix for specifics.
  • Units, user-defined functions, and incremental recomputation are intentionally out of scope for this milestone.
  • Arrays are handled via NumPy; if you need bigints, complex numbers, or matrices, the helper layer will require extension.

Contributions and bug reports are welcome!

Metadata

Release files for mathjs-to-func 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mathjs-to-func 0.5.0
File Size Uploaded
mathjs_to_func-0.5.0.tar.gz 35.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mathjs-to-func 0.5.0
File Interpreter ABI Platform
mathjs_to_func-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 72.9 kB

Release files / mathjs_to_func-0.5.0.tar.gz

Download URL mathjs_to_func-0.5.0.tar.gz
Size 35.0 kB
Tags Source
SHA-256 checksum
How to use checksums
517cc9247705df94003ab9a12aa9ce184ceb68924491cf6cb954783958c2ff3e
BLAKE2b-256 checksum
How to use checksums
7c12d6bba7ea6cd1ef928dc931e12be3da9444a6ef2fd362beab3fd4fecb209c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / mathjs_to_func-0.5.0-py3-none-any.whl

Download URL mathjs_to_func-0.5.0-py3-none-any.whl
Size 37.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
70abcc668728518acbfcd5960380108a91bcf0739764b6e44396a7745065faea
BLAKE2b-256 checksum
How to use checksums
8a0701cb86eb1987537a476bee8f61cee56a869c00568bf9d5421e77b516aceb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page