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 subset of math.js operators (+, -, *, /, ^, %, unary plus/minus) and functions (min, max, sum, ifnull).
- 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.
The project uses uv for dependency and virtualenv management. From the repository root:
uv add mathjs-to-funcAn optional parse extra installs a JSON-to-math.js parser powered by Pydantic:
uv add mathjs-to-func --extra parsefrom mathjs_to_func import build_evaluator
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_source__) # Generated Python sourcebuild_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 |
Name of the expression whose computed value should be returned. |
include_source |
bool (optional) |
Attach the 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 and may be reused across invocations.
| Node | Notes |
|---|---|
ConstantNode |
numeric (number), boolean, or null literals |
SymbolNode |
validated identifiers; must be alphanumeric/underscore, starting with a letter/underscore |
OperatorNode |
add, subtract, multiply, divide, pow, mod, unary unaryPlus, unaryMinus |
FunctionNode |
min, max, sum, ifnull |
ParenthesisNode |
forwards to the wrapped expression |
ArrayNode |
materialised to Python lists/NumPy arrays |
Unknown node types, invalid identifiers, or disallowed functions raise InvalidNodeError during compilation.
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.
All exceptions provide enough context (expression name, offending identifier, cycle list, etc.) to surface descriptive UI errors.
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}) # -> 42All examples below assume commands are wrapped with uv run ... to execute inside the managed environment.
- AST translation –
MathJsAstBuilderwalks the math.js JSON and emits Pythonast.ASTnodes. Identifiers are validated via a strict regex to prevent sneaky names like__import__. - Dependency graph – A topological sorter (
graphlib.TopologicalSorter) runs over expression references to produce a safe evaluation order while catching cycles and missing references upfront. - 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.
- Execution sandbox – The compiled module is executed with a tightly scoped globals dictionary: helper math functions, NumPy, and a few safe built-ins only. There is no ambient
__builtins__exposure. - Helper functions – math.js functions map onto small Python helpers (
_mj_min,_mj_max,_mj_sum,_mj_ifnull) that understand scalars and NumPy arrays.
Run the full suite (178 tests) with:
uv run pytestThe tests cover operator translation, helper semantics, dependency validation, error conditions, numpy-friendly behaviour, and public API ergonomics.
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 min/max/sum/ifnull
└── py.typed # PEP 561 marker for type-aware consumers
Additional documentation lives in docs/api_design.md, outlining the initial design considerations.
- Only a subset of math.js functions/operators are implemented today.
- 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!