{"id":32747787,"url":"https://github.com/hbmartin/mathjs-to-func","last_synced_at":"2026-05-17T19:07:05.071Z","repository":{"id":321789826,"uuid":"1087135275","full_name":"hbmartin/mathjs-to-func","owner":"hbmartin","description":"Compile serialized math.js expression trees into fast, reusable Python callables. Optional parse extra.","archived":false,"fork":false,"pushed_at":"2025-10-31T15:20:06.000Z","size":62,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-31T15:30:27.826Z","etag":null,"topics":["ast","compiler","expression-evaluator","expression-parser","expressions","formula","formula-parser","formulae","mathjs","numpy"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hbmartin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-31T12:37:49.000Z","updated_at":"2025-10-31T15:17:58.000Z","dependencies_parsed_at":"2025-11-02T21:01:07.946Z","dependency_job_id":null,"html_url":"https://github.com/hbmartin/mathjs-to-func","commit_stats":null,"previous_names":["hbmartin/mathjs-to-func"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/hbmartin/mathjs-to-func","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hbmartin%2Fmathjs-to-func","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hbmartin%2Fmathjs-to-func/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hbmartin%2Fmathjs-to-func/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hbmartin%2Fmathjs-to-func/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hbmartin","download_url":"https://codeload.github.com/hbmartin/mathjs-to-func/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hbmartin%2Fmathjs-to-func/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":282521097,"owners_count":26683141,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-03T02:00:05.676Z","response_time":108,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ast","compiler","expression-evaluator","expression-parser","expressions","formula","formula-parser","formulae","mathjs","numpy"],"created_at":"2025-11-03T20:01:12.329Z","updated_at":"2026-05-17T19:07:05.064Z","avatar_url":"https://github.com/hbmartin.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mathjs-to-func\n\n[![PyPI](https://img.shields.io/pypi/v/mathjs-to-func.svg)](https://pypi.org/project/mathjs-to-func/)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)\n[![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)\n[![CI](https://github.com/hbmartin/mathjs-to-func/actions/workflows/ci.yml/badge.svg)](https://github.com/hbmartin/mathjs-to-func/actions/workflows/ci.yml)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hbmartin/mathjs-to-func)\n\nA tiny Python library that compiles serialized [math.js](https://mathjs.org/) 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.\n\n## Key Features\n- Execute without reparsing or repeatedly walking the JSON graph.\n- Detect dependency cycles and missing identifiers early.\n- Keep execution sandboxed by compiling a controlled Python AST.\n- Work well with scalars or NumPy arrays for vectorised workloads.\n- Resolve common math.js constants like `pi`, `e`, `tau`, `NaN`, and `Infinity`.\n\n## Installation\n\nThe project uses [uv](https://github.com/astral-sh/uv) for dependency and virtualenv management. From the repository root:\n\n```bash\nuv add mathjs-to-func\n```\n\nAn optional `parse` extra installs a JSON-to-math.js parser powered by Pydantic:\n\n```bash\nuv add mathjs-to-func --extra parse\n```\n\n## Compiling A Function\n\n```python\nfrom mathjs_to_func import build_evaluator, to_dot, to_string\n\ndef main():\n    mathjs_payload = {\n        \"expressions\": {\n            # z = (x + y) / 2\n            \"sum_xy\": {\n                \"type\": \"OperatorNode\",\n                \"fn\": \"add\",\n                \"args\": [\n                    {\"type\": \"SymbolNode\", \"name\": \"x\"},\n                    {\"type\": \"SymbolNode\", \"name\": \"y\"},\n                ],\n            },\n            \"mean\": {\n                \"type\": \"OperatorNode\",\n                \"fn\": \"divide\",\n                \"args\": [\n                    {\"type\": \"SymbolNode\", \"name\": \"sum_xy\"},\n                    {\"type\": \"ConstantNode\", \"value\": \"2\", \"valueType\": \"number\"},\n                ],\n            },\n        },\n        \"inputs\": [\"x\", \"y\"],\n        \"target\": \"mean\",\n    }\n\n    evaluator = build_evaluator(**mathjs_payload, include_source=True)\n\n    result = evaluator({\"x\": 10, \"y\": 6})\n    print(result)  # -\u003e 8.0\n\n    # Introspection helpers\n    print(evaluator.__mathjs_required_inputs__)     # ('x', 'y')\n    print(evaluator.__mathjs_evaluation_order__)    # ('sum_xy', 'mean')\n    print(evaluator.__mathjs_inputs_referenced_per_target__)  # {'mean': ('x', 'y')}\n    print(evaluator.__mathjs_source__)              # Generated Python source\n    print(to_string(mathjs_payload))                 # sum_xy / 2\n    print(to_dot(mathjs_payload))                    # Dependency graph\n```\n\n### Parameters\n\n`build_evaluator` accepts keyword parameters (or a single `payload` mapping containing the same keys):\n\n| Argument      | Type                               | Description |\n|---------------|------------------------------------|-------------|\n| `expressions` | `Mapping[str, Mapping[str, Any]]`   | math.js AST JSON keyed by expression id. Each id becomes a local variable in the compiled function. |\n| `inputs`      | `Iterable[str]`                     | Whitelisted identifiers that may be supplied when the function is invoked. |\n| `target`      | `str \\| Sequence[str]`              | Name of the expression to return, or multiple expression names to return as a `dict[str, Any]`. |\n| `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. |\n| `compile_cache` | `bool` (optional)                | Reuse compiled functions through an opt-in structural LRU cache. |\n| `compile_cache_maxsize` | `int \\| None` (optional) | Cache size when `compile_cache=True`; defaults to `128`, and `None` makes the cache unbounded. |\n| `include_source` | `bool` (optional)              | Attach executable generated Python source code as `__mathjs_source__` on the returned callable. |\n\nThe 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.\n\nWhen `target` is a sequence, the callable returns a mapping in the requested target order:\n\n```python\nexprs = {\n    \"low\": {\n        \"type\": \"OperatorNode\",\n        \"fn\": \"subtract\",\n        \"args\": [\n            {\"type\": \"SymbolNode\", \"name\": \"x\"},\n            {\"type\": \"ConstantNode\", \"value\": \"2\", \"valueType\": \"number\"},\n        ],\n    },\n    \"high\": {\n        \"type\": \"OperatorNode\",\n        \"fn\": \"add\",\n        \"args\": [\n            {\"type\": \"SymbolNode\", \"name\": \"x\"},\n            {\"type\": \"ConstantNode\", \"value\": \"2\", \"valueType\": \"number\"},\n        ],\n    },\n}\n\nevaluator = build_evaluator(\n    expressions=exprs,\n    inputs=[\"x\"],\n    target=[\"low\", \"high\"],\n    include_source=True,\n)\nassert evaluator({\"x\": 10}) == {\"low\": 8, \"high\": 12}\n```\n\nThe source string includes the import preamble needed to re-execute it in an environment where `mathjs_to_func` is installed:\n\n```python\nnamespace = {}\nexec(evaluator.__mathjs_source__, namespace)\nassert namespace[\"_compiled\"]({\"x\": 10}) == evaluator({\"x\": 10})\n```\n\n### Supported math.js nodes\n\n| Node                     | Notes |\n|-------------------------|-------|\n| `ConstantNode`          | numeric (`number`), string, boolean, or `null` literals |\n| `SymbolNode`            | inputs, expression references, and common built-in constants; identifiers must be alphanumeric/underscore, starting with a letter/underscore |\n| `OperatorNode`          | `add`, `subtract`, `multiply`, `divide`, `pow`, `mod`, unary `unaryPlus`, `unaryMinus`, `not`, `and`, `or`, `xor`, comparisons, and `nullish` |\n| `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)` |\n| `ParenthesisNode`       | forwards to the wrapped expression |\n| `ArrayNode`             | materialised to Python lists/NumPy arrays |\n| `AccessorNode`/`IndexNode` | read-only indexing with math.js 1-based indices translated to Python 0-based indices |\n| `RangeNode`             | materialised to inclusive NumPy ranges with optional non-zero step |\n| `ObjectNode`            | materialised to Python dict literals with string keys |\n| `ConditionalNode`       | lazy scalar ternary evaluation, vectorised NumPy `where` for arrays |\n| `RelationalNode`        | chained comparisons like `10 \u003c x \u003c= 50`, with scalar short-circuiting |\n\nUnknown node types, invalid identifiers, or disallowed functions raise `InvalidNodeError` during compilation.\n\nSee [docs/compatibility.md](docs/compatibility.md) for the fuller math.js compatibility matrix and known gaps.\n\n### Error handling\n\n- `ExpressionError`: base class for configuration mistakes.\n- `MissingTargetError`: requested target id does not exist.\n- `UnknownIdentifierError`: an expression references a symbol that is neither an input nor another expression.\n- `CircularDependencyError`: dependency graph contains a cycle.\n- `InvalidNodeError`: AST contains unsupported structures or invalid literals.\n- `InputValidationError`: the compiled function received inputs that are missing, unexpected, or not a mapping.\n- `RuntimeEvaluationError`: a compiled expression failed at runtime; the original exception is preserved as `__cause__`.\n\nAll exceptions provide enough context (`expression` name, offending identifier, cycle list, etc.) to surface descriptive UI errors.\n\n## Parsing math.js JSON\n\nWith the extra installed you can turn serialized math.js nodes into evaluator-ready mappings:\n\n```python\nfrom mathjs_to_func import build_evaluator\nfrom mathjs_to_func.parse import parse\n\nexpression = parse(\n    \"\"\"{\n    \"type\": \"OperatorNode\",\n    \"fn\": \"add\",\n    \"args\": [\n        {\"type\": \"SymbolNode\", \"name\": \"x\"},\n        {\"type\": \"ConstantNode\", \"value\": \"2\", \"valueType\": \"number\"}\n    ]\n}\"\"\"\n)\n\nevaluator = build_evaluator(\n    expressions={\"total\": expression},\n    inputs=[\"x\"],\n    target=\"total\",\n)\n\nresult = evaluator({\"x\": 40})  # -\u003e 42\n```\n\nFor complete `{expressions, inputs, target}` envelopes, use `parse_payload`:\n\n```python\nfrom mathjs_to_func import build_evaluator\nfrom mathjs_to_func.parse import parse_payload\n\npayload = parse_payload(serialized_payload)\nevaluator = build_evaluator(payload=payload)\n```\n\nThe 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.\n\nAll examples below assume commands are wrapped with `uv run ...` to execute inside the managed environment.\n\n## CLI\n\nCompile a payload file and inspect the generated Python source without writing a script:\n\n```bash\nuv run python -m mathjs_to_func compile payload.json --target z --emit-source\n```\n\nWithout `--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.\n\n## JSON Schema\n\nExport JSON Schema for frontend validation of serialized math.js payloads:\n\n```bash\nuv run python -m mathjs_to_func schema --output dist/mathjs-to-func.schema.json\n```\n\nThe 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.\n\n## Implementation Notes\n\n1. **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.\n2. **Dependency graph** – A topological sorter (`graphlib.TopologicalSorter`) runs over expression references to produce a safe evaluation order while catching cycles and missing references upfront.\n3. **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.\n4. **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.\n5. **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.\n\n### Introspection\n\nCompiled 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.\n\n### Cache Notes\n\n`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`.\n\n## Testing\n\nRun the full suite with:\n\n```bash\nuv run pytest\n```\n\nRun the benchmark suite locally with:\n\n```bash\nnpm ci --prefix bench/js\nuv run python -m bench\n```\n\nCI 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.\n\nRun mutation testing with:\n\n```bash\nuv run mutmut run\nuv run mutmut results\n```\n\nThe GitHub mutation workflow runs on source and test changes, records the full mutmut result set, and emits a warning when any mutants survive.\n\nThe tests cover operator translation, helper semantics, dependency validation, error conditions, numpy-friendly behaviour, and public API ergonomics.\n\n## Project Structure\n\n```\nsrc/mathjs_to_func/\n├── __init__.py          # build_evaluator public API and export list\n├── ast_builder.py       # math.js JSON → Python AST translation\n├── compiler.py          # dependency graph, code generation, compilation\n├── errors.py            # structured exception hierarchy\n├── helpers.py           # runtime helpers for math.js-compatible functions/operators\n└── py.typed             # PEP 561 marker for type-aware consumers\n```\n\nAdditional documentation lives in `docs/api_design.md`, outlining the initial design considerations.\n\n## Limitations \u0026 Future Work\n\n- Only a subset of math.js functions/operators are implemented today; see the compatibility matrix for specifics.\n- Units, user-defined functions, and incremental recomputation are intentionally out of scope for this milestone.\n- Arrays are handled via NumPy; if you need bigints, complex numbers, or matrices, the helper layer will require extension.\n\nContributions and bug reports are welcome!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhbmartin%2Fmathjs-to-func","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhbmartin%2Fmathjs-to-func","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhbmartin%2Fmathjs-to-func/lists"}