{"id":34570941,"url":"https://github.com/queelius/rerum","last_synced_at":"2026-04-12T10:10:40.712Z","repository":{"id":330260251,"uuid":"1117131762","full_name":"queelius/rerum","owner":"queelius","description":"RERUM - Rewriting Expressions via Rules Using Morphisms. A pattern matching and term rewriting library for symbolic computation.","archived":false,"fork":false,"pushed_at":"2026-01-04T10:29:22.000Z","size":721,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-13T20:40:14.013Z","etag":null,"topics":["cli","dsl","pattern-matching","python","repl","s-expressions","symbolic-computation","term-rewriting"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/queelius.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"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-12-15T21:59:39.000Z","updated_at":"2026-01-07T18:15:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/queelius/rerum","commit_stats":null,"previous_names":["queelius/rerum"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/queelius/rerum","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/queelius%2Frerum","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/queelius%2Frerum/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/queelius%2Frerum/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/queelius%2Frerum/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/queelius","download_url":"https://codeload.github.com/queelius/rerum/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/queelius%2Frerum/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31711012,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-12T06:22:27.080Z","status":"ssl_error","status_checked_at":"2026-04-12T06:21:52.710Z","response_time":58,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["cli","dsl","pattern-matching","python","repl","s-expressions","symbolic-computation","term-rewriting"],"created_at":"2025-12-24T09:35:29.416Z","updated_at":"2026-04-12T10:10:40.707Z","avatar_url":"https://github.com/queelius.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RERUM\n\n[![PyPI version](https://badge.fury.io/py/rerum.svg)](https://badge.fury.io/py/rerum)\n[![CI](https://github.com/queelius/rerum/actions/workflows/ci.yml/badge.svg)](https://github.com/queelius/rerum/actions/workflows/ci.yml)\n[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n**Rewriting Expressions via Rules Using Morphisms**\n\nA pattern matching and term rewriting library for symbolic computation in Python.\n\n## Installation\n\n```bash\npip install rerum\n```\n\n## Quick Start\n\n```python\nfrom rerum import RuleEngine, E\n\n# Create an engine with rules\nengine = RuleEngine.from_dsl('''\n    @add-zero \"x + 0 = x\": (+ ?x 0) =\u003e :x\n    @mul-one: (* ?x 1) =\u003e :x\n    @mul-zero: (* ?x 0) =\u003e 0\n''')\n\n# Simplify expressions using E() to parse s-expressions\nengine(E(\"(+ y 0)\"))           # =\u003e \"y\"\nengine(E(\"(* x 1)\"))           # =\u003e \"x\"\nengine(E(\"(* (+ a 0) 0)\"))     # =\u003e 0\n\n# Or use raw lists\nengine([\"+\", \"y\", 0])          # =\u003e \"y\"\n```\n\n## DSL Syntax\n\nRules use a simple, readable syntax:\n\n```\n# Comments start with #\n@rule-name: (pattern) =\u003e (skeleton)\n@rule-name \"Description\": (pattern) =\u003e (skeleton)\n@rule-name[100]: (pattern) =\u003e (skeleton)           # With priority\n@rule-name: (pattern) =\u003e (skeleton) when (cond)    # With guard\n```\n\n### Pattern Syntax\n\n| Syntax | Meaning |\n|--------|---------|\n| `?x` or `?x:expr` | Match any expression, bind to x |\n| `?x:const` | Match constant (number) only |\n| `?x:var` | Match variable (symbol) only |\n| `?x:free(v)` | Match expression not containing v |\n| `?x...` | Match zero or more remaining args (rest pattern) |\n\n### Skeleton Syntax\n\n| Syntax | Meaning |\n|--------|---------|\n| `:x` | Substitute bound value of x |\n| `:x...` | Splice list bound to x |\n| `(! op args...)` | Compute: evaluate op with args using prelude |\n\n## Expression Builder\n\nThe `E` builder provides convenient expression construction:\n\n```python\nfrom rerum import E\n\n# Parse s-expression strings\nexpr = E(\"(+ x (* 2 y))\")      # =\u003e [\"+\", \"x\", [\"*\", 2, \"y\"]]\n\n# Build programmatically\nexpr = E.op(\"+\", \"x\", E.op(\"*\", 2, \"y\"))\n\n# Create variables\nx, y = E.vars(\"x\", \"y\")\nexpr = E.op(\"+\", x, E.op(\"*\", 2, y))\n```\n\n## Conditional Guards\n\nRules can have conditions that must be satisfied:\n\n```python\nfrom rerum import RuleEngine, E, FULL_PRELUDE\n\nengine = (RuleEngine()\n    .with_prelude(FULL_PRELUDE)\n    .load_dsl('''\n        @abs-pos: (abs ?x) =\u003e :x when (! \u003e :x 0)\n        @abs-neg: (abs ?x) =\u003e (! - 0 :x) when (! \u003c :x 0)\n        @abs-zero: (abs ?x) =\u003e 0 when (! = :x 0)\n    '''))\n\nengine(E(\"(abs 5)\"))   # =\u003e 5\nengine(E(\"(abs -5)\"))  # =\u003e 5\nengine(E(\"(abs 0)\"))   # =\u003e 0\n```\n\nGuards use the `(! ...)` compute syntax and have access to type predicates:\n- `const?` - true for numbers\n- `var?` - true for symbols/variables\n- `list?` - true for compound expressions\n- Comparison: `\u003e`, `\u003c`, `\u003e=`, `\u003c=`, `=`, `!=`\n- Logical: `and`, `or`, `not`\n\n## Rule Priorities\n\nHigher priority rules fire first:\n\n```python\nengine = RuleEngine.from_dsl('''\n    @general: (+ ?x ?y) =\u003e (add :x :y)\n    @specific[100]: (+ 0 ?x) =\u003e :x        # Fires first\n    @specific2[100]: (+ ?x 0) =\u003e :x       # Fires first\n''')\n\nengine(E(\"(+ 0 y)\"))  # =\u003e \"y\" (specific rule wins)\nengine(E(\"(+ a b)\"))  # =\u003e [\"add\", \"a\", \"b\"] (general rule)\n```\n\n## Named Rulesets (Groups)\n\nOrganize rules into groups and selectively enable them:\n\n```python\nengine = RuleEngine.from_dsl('''\n    [algebra]\n    @add-zero: (+ ?x 0) =\u003e :x\n    @mul-one: (* ?x 1) =\u003e :x\n\n    [calculus]\n    @dd-const: (dd ?c:const ?v:var) =\u003e 0\n    @dd-var: (dd ?x:var ?x) =\u003e 1\n''')\n\n# Use only algebra rules\nengine(E(\"(+ x 0)\"), groups=[\"algebra\"])\n\n# Disable a group\nengine.disable_group(\"calculus\")\n\n# Get all group names\nengine.groups()  # =\u003e {\"algebra\", \"calculus\"}\n```\n\n## Rewriting Strategies\n\nControl how rules are applied:\n\n```python\n# exhaustive (default): Apply rules repeatedly until fixpoint\nresult = engine(expr, strategy=\"exhaustive\")\n\n# once: Apply at most one rule anywhere\nresult = engine(expr, strategy=\"once\")\n\n# bottomup: Simplify children first, then parent\nresult = engine(expr, strategy=\"bottomup\")\n\n# topdown: Try parent first, then children\nresult = engine(expr, strategy=\"topdown\")\n```\n\n## Tracing\n\nSee which rules are applied:\n\n```python\nresult, trace = engine(expr, trace=True)\nprint(trace)  # Verbose multi-line format\n\n# Different formats\nprint(trace.format(\"compact\"))  # Single line\nprint(trace.format(\"rules\"))    # Just rule names\nprint(trace.format(\"chain\"))    # Step-by-step chain\n\n# Statistics\nprint(trace.summary())          # Brief summary\nprint(trace.rule_counts())      # Rule usage counts\nprint(trace.rules_applied())    # List of rules in order\n\n# Serialization\nimport json\njson.dumps(trace.to_dict())\n```\n\n## Preludes and Constant Folding\n\nPreludes define computational primitives for the `(! op ...)` compute form:\n\n```python\nfrom rerum import RuleEngine, ARITHMETIC_PRELUDE, MATH_PRELUDE, FULL_PRELUDE\n\n# Fluent construction with prelude\nengine = (RuleEngine()\n    .with_prelude(ARITHMETIC_PRELUDE)\n    .load_dsl('''\n        @fold: (+ ?a:const ?b:const) =\u003e (! + :a :b)\n    '''))\n\nengine(E(\"(+ 1 2)\"))  # =\u003e 3\n```\n\n### Available Preludes\n\n| Prelude | Operations |\n|---------|------------|\n| `ARITHMETIC_PRELUDE` | `+`, `-`, `*`, `/`, `^` |\n| `MATH_PRELUDE` | Arithmetic + `sin`, `cos`, `tan`, `exp`, `log`, `sqrt`, `abs` |\n| `PREDICATE_PRELUDE` | `\u003e`, `\u003c`, `=`, `const?`, `var?`, `list?`, `and`, `or`, `not` |\n| `FULL_PRELUDE` | Arithmetic + Predicates |\n| `MINIMAL_PRELUDE` | `+`, `*` only |\n| `NO_PRELUDE` | Empty (pure symbolic rewriting) |\n\n### Custom Preludes\n\n```python\nfrom rerum import RuleEngine, nary_fold, unary_only, binary_only\n\nmy_prelude = {\n    \"+\": nary_fold(0, lambda a, b: a + b),      # n-ary with identity\n    \"max\": binary_only(max),                     # binary only\n    \"neg\": unary_only(lambda x: -x),            # unary only\n    \"gcd\": binary_only(math.gcd),               # custom function\n}\n\nengine = RuleEngine().with_prelude(my_prelude).load_dsl(rules)\n```\n\n## Engine Sequencing\n\nApply engines in phases:\n\n```python\nexpand = RuleEngine.from_dsl(\"@square: (square ?x) =\u003e (* :x :x)\")\nsimplify = RuleEngine.from_dsl(\"@fold: (* ?a:const ?b:const) =\u003e (! * :a :b)\",\n                                fold_funcs=ARITHMETIC_PRELUDE)\n\n# Sequence with \u003e\u003e\nnormalize = expand \u003e\u003e simplify\nnormalize(E(\"(square 3)\"))  # =\u003e 9\n\n# Chain multiple phases\npipeline = expand \u003e\u003e simplify \u003e\u003e another_engine\n```\n\n## Fluent API\n\n```python\nfrom rerum import RuleEngine, FULL_PRELUDE\n\nengine = (RuleEngine()\n    .with_prelude(FULL_PRELUDE)\n    .load_dsl('''\n        @add-zero: (+ ?x 0) =\u003e :x\n    ''')\n    .load_file(\"more_rules.rules\")\n    .add_rule(\n        pattern=E.op(\"+\", [\"?\", \"x\"], [\"?\", \"x\"]),\n        skeleton=E.op(\"*\", 2, [\":\", \"x\"]),\n        name=\"double\"\n    )\n    .disable_group(\"experimental\"))\n\n# Pattern matching\nif bindings := engine.match(\"(+ ?a ?b)\", expr):\n    print(bindings[\"a\"], bindings[\"b\"])\n\n# Apply single rule\nresult, meta = engine.apply_once(expr)\n\n# Find matching rules\nfor meta, bindings in engine.rules_matching(expr):\n    print(f\"Rule {meta.name} matches\")\n```\n\n## Variadic Patterns\n\nRest patterns (`?x...`) capture remaining arguments:\n\n```python\nengine = RuleEngine.from_dsl('''\n    # Flatten nested additions\n    @flatten-add: (+ (+ ?a ?b) ?rest...) =\u003e (+ :a :b :rest...)\n\n    # Constant folding with rest\n    @fold-add: (+ ?a:const ?b:const ?rest...) =\u003e (+ (! + :a :b) :rest...)\n''', fold_funcs=ARITHMETIC_PRELUDE)\n\nengine(E(\"(+ (+ 1 2) 3 4)\"))  # =\u003e [\"+\", 1, 2, 3, 4] =\u003e 10\n```\n\n## Equivalence, Proof, and Optimization\n\nBidirectional rules (`\u003c=\u003e`) let you reason over equivalence classes, not\njust reduce expressions to normal form.\n\n### Bidirectional Rules\n\n```python\nengine = RuleEngine.from_dsl('''\n    @comm-add:  (+ ?x ?y) \u003c=\u003e (+ :y :x)\n    @assoc:     (+ (+ ?x ?y) ?z) \u003c=\u003e (+ :x (+ :y :z))\n    @demorgan:  (not (and ?x ?y)) \u003c=\u003e (or (not :x) (not :y))\n''')\n```\n\nEach `\u003c=\u003e` rule expands into two unidirectional rules internally, so the\nequivalence class is closed under both directions.\n\n### Proving Equality\n\n`prove_equal` uses bidirectional BFS. It meets in the middle, so it handles\nnon-trivial equalities in milliseconds on small rule sets.\n\n```python\nproof = engine.prove_equal(\n    [\"+\", [\"+\", \"a\", \"b\"], \"c\"],\n    [\"+\", \"c\", [\"+\", \"b\", \"a\"]],\n    max_depth=10,\n    max_expressions=5000,   # optional work budget\n)\nif proof:\n    print(format_sexpr(proof.common))\n    print(f\"Depths: a={proof.depth_a}, b={proof.depth_b}\")\n\n# Boolean shortcut\nengine.are_equal(a, b)\n```\n\nSet `max_expressions` to bound un-provable queries (they otherwise exhaust\nthe full depth-bounded reachable set on both sides).\n\n### Minimizing Cost\n\n`minimize` searches the equivalence class for the lowest-cost member.\n\n```python\nfrom rerum import expr_size, expr_depth, make_op_cost_fn\n\n# Built-in metric\nresult = engine.minimize(expr, metric=\"size\")    # or \"depth\", \"ops\", \"atoms\"\n\n# Custom cost\nresult = engine.minimize(expr, cost=lambda e: expr_size(e) + 2*expr_depth(e))\n\n# Per-operator costs\nresult = engine.minimize(expr, op_costs={\"+\": 1, \"*\": 2, \"^\": 10})\n\nprint(result.expr, result.cost, result.improvement_ratio)\n```\n\nBy default, `minimize` uses both `=\u003e` and `\u003c=\u003e` rules, which matches how\nusers typically write simplification rules. Pass `include_unidirectional=False`\nto restrict to strict reversible equivalences.\n\n### Enumerating and Sampling\n\n```python\n# Lazy generator over the equivalence class\nfor eq in engine.equivalents(expr, max_depth=6):\n    ...\n\n# Eager list\nforms = engine.enumerate_equivalents(expr, max_depth=10, max_count=1000)\n\n# Random sampling\nimport random\nrng = random.Random(42)\nsamples = engine.sample_equivalents(expr, n=10, unique=True, rng=rng)\nequiv = engine.random_equivalent(expr, steps=20, rng=rng)\n```\n\nUnder `assoc + commute`, the class of an `n`-term sum has exactly\n`n! × Catalan(n-1)` members (2, 12, 120, 1680 for `n = 2..5`). Past `n = 5`\nprefer `prove_equal` with a budget over full enumeration. See the\n[equivalence guide](https://queelius.github.io/rerum/equivalence/) and\n`experiments/` for benchmarks.\n\n## API Reference\n\n### Creating Engines\n\n```python\n# From DSL text\nengine = RuleEngine.from_dsl(\"@add-zero: (+ ?x 0) =\u003e :x\")\n\n# From file\nengine = RuleEngine.from_file(\"rules.rules\")  # DSL format\nengine = RuleEngine.from_file(\"rules.json\")   # JSON format\n\n# From Python lists\nrules = [[[\"+\", [\"?\", \"x\"], 0], [\":\", \"x\"]]]\nengine = RuleEngine.from_rules(rules)\n\n# With prelude\nengine = RuleEngine.from_dsl(dsl_text, fold_funcs=ARITHMETIC_PRELUDE)\n```\n\n### Using Engines\n\n```python\n# Simplify (callable shorthand)\nresult = engine(expr)\n\n# With options\nresult = engine(expr, strategy=\"bottomup\", groups=[\"algebra\"])\n\n# With tracing\nresult, trace = engine(expr, trace=True)\n```\n\n### Inspecting Engines\n\n```python\nlen(engine)                  # Number of rules\n\"add-zero\" in engine         # Check if rule exists\nrule, meta = engine[\"add-zero\"]  # Get by name\nengine.list_rules()          # DSL format strings\nengine.groups()              # All group names\n\nfor rule, meta in engine:    # Iterate\n    print(meta.name, meta.description)\n```\n\n### Combining Engines\n\n```python\nalgebra = RuleEngine.from_file(\"algebra.rules\")\ncalculus = RuleEngine.from_file(\"calculus.rules\")\n\ncombined = algebra | calculus  # Union\nalgebra |= calculus            # In-place union\nphased = algebra \u003e\u003e calculus   # Sequence (algebra first, then calculus)\n```\n\n## JSON Format\n\nRules can also be loaded from JSON:\n\n```json\n{\n    \"name\": \"algebra\",\n    \"description\": \"Basic algebraic rules\",\n    \"rules\": [\n        {\n            \"name\": \"add-zero\",\n            \"description\": \"x + 0 = x\",\n            \"pattern\": [\"+\", [\"?\", \"x\"], 0],\n            \"skeleton\": [\":\", \"x\"]\n        }\n    ]\n}\n```\n\n## Architecture\n\n```\n+-------------------------------------+\n|  Rules (DSL/JSON) - Serializable    |\n|  - Pattern matching                 |\n|  - Symbolic transformation          |\n|  - (! op ...) references operations |\n|  - Conditions reference predicates  |\n+------------------+------------------+\n                   | references\n                   v\n+-------------------------------------+\n|  Prelude (Python) - Trusted Code    |\n|  - Defines what operations do       |\n|  - Provided by developer            |\n|  - Cannot be injected from rules    |\n+-------------------------------------+\n```\n\nRules loaded from untrusted sources cannot execute arbitrary code - they can only invoke operations explicitly enabled in the prelude.\n\n## Low-Level API\n\nFor advanced use cases:\n\n```python\nfrom rerum import rewriter, match, instantiate, parse_sexpr, format_sexpr\n\n# Create a rewriter function directly\nsimplify = rewriter(rules, fold_funcs=ARITHMETIC_PRELUDE)\nresult = simplify(expr)\n\n# Pattern matching\nbindings = match(pattern, expr, [])\nif bindings != \"failed\":\n    result = instantiate(skeleton, bindings, fold_funcs)\n\n# S-expression parsing\nexpr = parse_sexpr(\"(+ x (* 2 y))\")  # =\u003e [\"+\", \"x\", [\"*\", 2, \"y\"]]\ntext = format_sexpr(expr)             # =\u003e \"(+ x (* 2 y))\"\n```\n\n## Command-Line Interface\n\nRERUM includes a CLI for interactive use and scripting.\n\n### REPL Mode\n\n```bash\n$ rerum\nrerum\u003e @add-zero: (+ ?x 0) =\u003e :x\nAdded 1 rule(s)\nrerum\u003e (+ y 0)\ny\nrerum\u003e :quit\n```\n\n### Script Mode\n\nCreate a `.rerum` file:\n\n```bash\n#!/usr/bin/env rerum\n:prelude full\n\n# Symbolic rule: transforms structure, no computation\n@square: (square ?x) =\u003e (* :x :x)\n\n# Computation rule: (!) evaluates when args are constants\n@fold-mul: (* ?a ?b) =\u003e (! * :a :b) when (! and (! const? :a) (! const? :b))\n\n# (square x) =\u003e (* x x)  (symbolic, x stays as x)\n(square x)\n\n# (square 5) =\u003e (* 5 5) =\u003e 25  (fold-mul computes the result)\n(square 5)\n```\n\nOutput:\n```\n(* x x)\n25\n```\n\nRun scripts:\n```bash\n$ rerum script.rerum\n$ chmod +x script.rerum \u0026\u0026 ./script.rerum  # With shebang\n```\n\n### One-Shot Mode\n\n```bash\n$ rerum -r rules.rules -p full -e \"(+ x 0)\"\nx\n```\n\n### Pipe Mode\n\n```bash\n$ echo \"(+ x 0)\" | rerum -r rules.rules -p full -q\nx\n```\n\n### CLI Options\n\n```\nrerum [script]              Run a script or start REPL\n  -r, --rules FILE          Load rules (can repeat)\n  -e, --expr EXPR           Evaluate single expression\n  -p, --prelude NAME        Set prelude (arithmetic, math, full, none, or path.py)\n  -t, --trace               Enable tracing\n  -s, --strategy NAME       Strategy: exhaustive, once, bottomup, topdown\n  -q, --quiet               Suppress non-essential output\n```\n\n### REPL Commands\n\n```\n:help              Show help\n:load FILE         Load rules from file\n:rules             List loaded rules\n:clear             Clear all rules\n:prelude NAME      Set prelude\n:trace on|off      Toggle tracing\n:strategy NAME     Set rewriting strategy\n:groups            Show groups\n:enable GROUP      Enable a group\n:disable GROUP     Disable a group\n:quit              Exit\n```\n\n### Custom Preludes\n\nCreate a Python file with a `PRELUDE` dict:\n\n```python\n# my_prelude.py\nfrom rerum import binary_only, unary_only\nimport math\n\nPRELUDE = {\n    \"gcd\": binary_only(math.gcd),\n    \"factorial\": unary_only(math.factorial),\n}\n```\n\nUse it:\n```bash\n$ rerum -p my_prelude.py -r rules.rules\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqueelius%2Frerum","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fqueelius%2Frerum","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqueelius%2Frerum/lists"}