RERUM - Rewriting Expressions via Rules Using Morphisms
Project description
RERUM
Rewriting Expressions via Rules Using Morphisms
A pattern matching and term rewriting library for symbolic computation in Python.
Installation
pip install rerum
Quick Start
from rerum import RuleEngine, E
# Create an engine with rules
engine = RuleEngine.from_dsl('''
@add-zero "x + 0 = x": (+ ?x 0) => :x
@mul-one: (* ?x 1) => :x
@mul-zero: (* ?x 0) => 0
''')
# Simplify expressions using E() to parse s-expressions
engine(E("(+ y 0)")) # => "y"
engine(E("(* x 1)")) # => "x"
engine(E("(* (+ a 0) 0)")) # => 0
# Or use raw lists
engine(["+", "y", 0]) # => "y"
DSL Syntax
Rules use a simple, readable syntax:
# Comments start with #
@rule-name: (pattern) => (skeleton)
@rule-name "Description": (pattern) => (skeleton)
@rule-name[100]: (pattern) => (skeleton) # With priority
@rule-name: (pattern) => (skeleton) when (cond) # With guard
Pattern Syntax
| Syntax | Meaning |
|---|---|
?x or ?x:expr |
Match any expression, bind to x |
?x:const |
Match constant (number) only |
?x:var |
Match variable (symbol) only |
?x:free(v) |
Match expression not containing v |
?x... |
Match zero or more remaining args (rest pattern) |
Skeleton Syntax
| Syntax | Meaning |
|---|---|
:x |
Substitute bound value of x |
:x... |
Splice list bound to x |
(! op args...) |
Compute: evaluate op with args using prelude |
Expression Builder
The E builder provides convenient expression construction:
from rerum import E
# Parse s-expression strings
expr = E("(+ x (* 2 y))") # => ["+", "x", ["*", 2, "y"]]
# Build programmatically
expr = E.op("+", "x", E.op("*", 2, "y"))
# Create variables
x, y = E.vars("x", "y")
expr = E.op("+", x, E.op("*", 2, y))
Conditional Guards
Rules can have conditions that must be satisfied:
from rerum import RuleEngine, E, FULL_PRELUDE
engine = (RuleEngine()
.with_prelude(FULL_PRELUDE)
.load_dsl('''
@abs-pos: (abs ?x) => :x when (! > :x 0)
@abs-neg: (abs ?x) => (! - 0 :x) when (! < :x 0)
@abs-zero: (abs ?x) => 0 when (! = :x 0)
'''))
engine(E("(abs 5)")) # => 5
engine(E("(abs -5)")) # => 5
engine(E("(abs 0)")) # => 0
Guards use the (! ...) compute syntax and have access to type predicates:
const?- true for numbersvar?- true for symbols/variableslist?- true for compound expressions- Comparison:
>,<,>=,<=,=,!= - Logical:
and,or,not
Rule Priorities
Higher priority rules fire first:
engine = RuleEngine.from_dsl('''
@general: (+ ?x ?y) => (add :x :y)
@specific[100]: (+ 0 ?x) => :x # Fires first
@specific2[100]: (+ ?x 0) => :x # Fires first
''')
engine(E("(+ 0 y)")) # => "y" (specific rule wins)
engine(E("(+ a b)")) # => ["add", "a", "b"] (general rule)
Named Rulesets (Groups)
Organize rules into groups and selectively enable them:
engine = RuleEngine.from_dsl('''
[algebra]
@add-zero: (+ ?x 0) => :x
@mul-one: (* ?x 1) => :x
[calculus]
@dd-const: (dd ?c:const ?v:var) => 0
@dd-var: (dd ?x:var ?x) => 1
''')
# Use only algebra rules
engine(E("(+ x 0)"), groups=["algebra"])
# Disable a group
engine.disable_group("calculus")
# Get all group names
engine.groups() # => {"algebra", "calculus"}
Rewriting Strategies
Control how rules are applied:
# exhaustive (default): Apply rules repeatedly until fixpoint
result = engine(expr, strategy="exhaustive")
# once: Apply at most one rule anywhere
result = engine(expr, strategy="once")
# bottomup: Simplify children first, then parent
result = engine(expr, strategy="bottomup")
# topdown: Try parent first, then children
result = engine(expr, strategy="topdown")
Tracing
See which rules are applied:
result, trace = engine(expr, trace=True)
print(trace) # Verbose multi-line format
# Different formats
print(trace.format("compact")) # Single line
print(trace.format("rules")) # Just rule names
print(trace.format("chain")) # Step-by-step chain
# Statistics
print(trace.summary()) # Brief summary
print(trace.rule_counts()) # Rule usage counts
print(trace.rules_applied()) # List of rules in order
# Serialization
import json
json.dumps(trace.to_dict())
Preludes and Constant Folding
Preludes define computational primitives for the (! op ...) compute form:
from rerum import RuleEngine, ARITHMETIC_PRELUDE, MATH_PRELUDE, FULL_PRELUDE
# Fluent construction with prelude
engine = (RuleEngine()
.with_prelude(ARITHMETIC_PRELUDE)
.load_dsl('''
@fold: (+ ?a:const ?b:const) => (! + :a :b)
'''))
engine(E("(+ 1 2)")) # => 3
Available Preludes
| Prelude | Operations |
|---|---|
ARITHMETIC_PRELUDE |
+, -, *, /, ^ |
MATH_PRELUDE |
Arithmetic + sin, cos, tan, exp, log, sqrt, abs |
PREDICATE_PRELUDE |
>, <, =, const?, var?, list?, and, or, not |
FULL_PRELUDE |
Arithmetic + Predicates |
MINIMAL_PRELUDE |
+, * only |
NO_PRELUDE |
Empty (pure symbolic rewriting) |
Custom Preludes
from rerum import RuleEngine, nary_fold, unary_only, binary_only
my_prelude = {
"+": nary_fold(0, lambda a, b: a + b), # n-ary with identity
"max": binary_only(max), # binary only
"neg": unary_only(lambda x: -x), # unary only
"gcd": binary_only(math.gcd), # custom function
}
engine = RuleEngine().with_prelude(my_prelude).load_dsl(rules)
Engine Sequencing
Apply engines in phases:
expand = RuleEngine.from_dsl("@square: (square ?x) => (* :x :x)")
simplify = RuleEngine.from_dsl("@fold: (* ?a:const ?b:const) => (! * :a :b)",
fold_funcs=ARITHMETIC_PRELUDE)
# Sequence with >>
normalize = expand >> simplify
normalize(E("(square 3)")) # => 9
# Chain multiple phases
pipeline = expand >> simplify >> another_engine
Fluent API
from rerum import RuleEngine, FULL_PRELUDE
engine = (RuleEngine()
.with_prelude(FULL_PRELUDE)
.load_dsl('''
@add-zero: (+ ?x 0) => :x
''')
.load_file("more_rules.rules")
.add_rule(
pattern=E.op("+", ["?", "x"], ["?", "x"]),
skeleton=E.op("*", 2, [":", "x"]),
name="double"
)
.disable_group("experimental"))
# Pattern matching
if bindings := engine.match("(+ ?a ?b)", expr):
print(bindings["a"], bindings["b"])
# Apply single rule
result, meta = engine.apply_once(expr)
# Find matching rules
for meta, bindings in engine.rules_matching(expr):
print(f"Rule {meta.name} matches")
Variadic Patterns
Rest patterns (?x...) capture remaining arguments:
engine = RuleEngine.from_dsl('''
# Flatten nested additions
@flatten-add: (+ (+ ?a ?b) ?rest...) => (+ :a :b :rest...)
# Constant folding with rest
@fold-add: (+ ?a:const ?b:const ?rest...) => (+ (! + :a :b) :rest...)
''', fold_funcs=ARITHMETIC_PRELUDE)
engine(E("(+ (+ 1 2) 3 4)")) # => ["+", 1, 2, 3, 4] => 10
API Reference
Creating Engines
# From DSL text
engine = RuleEngine.from_dsl("@add-zero: (+ ?x 0) => :x")
# From file
engine = RuleEngine.from_file("rules.rules") # DSL format
engine = RuleEngine.from_file("rules.json") # JSON format
# From Python lists
rules = [[["+", ["?", "x"], 0], [":", "x"]]]
engine = RuleEngine.from_rules(rules)
# With prelude
engine = RuleEngine.from_dsl(dsl_text, fold_funcs=ARITHMETIC_PRELUDE)
Using Engines
# Simplify (callable shorthand)
result = engine(expr)
# With options
result = engine(expr, strategy="bottomup", groups=["algebra"])
# With tracing
result, trace = engine(expr, trace=True)
Inspecting Engines
len(engine) # Number of rules
"add-zero" in engine # Check if rule exists
rule, meta = engine["add-zero"] # Get by name
engine.list_rules() # DSL format strings
engine.groups() # All group names
for rule, meta in engine: # Iterate
print(meta.name, meta.description)
Combining Engines
algebra = RuleEngine.from_file("algebra.rules")
calculus = RuleEngine.from_file("calculus.rules")
combined = algebra | calculus # Union
algebra |= calculus # In-place union
phased = algebra >> calculus # Sequence (algebra first, then calculus)
JSON Format
Rules can also be loaded from JSON:
{
"name": "algebra",
"description": "Basic algebraic rules",
"rules": [
{
"name": "add-zero",
"description": "x + 0 = x",
"pattern": ["+", ["?", "x"], 0],
"skeleton": [":", "x"]
}
]
}
Architecture
+-------------------------------------+
| Rules (DSL/JSON) - Serializable |
| - Pattern matching |
| - Symbolic transformation |
| - (! op ...) references operations |
| - Conditions reference predicates |
+------------------+------------------+
| references
v
+-------------------------------------+
| Prelude (Python) - Trusted Code |
| - Defines what operations do |
| - Provided by developer |
| - Cannot be injected from rules |
+-------------------------------------+
Rules loaded from untrusted sources cannot execute arbitrary code - they can only invoke operations explicitly enabled in the prelude.
Low-Level API
For advanced use cases:
from rerum import rewriter, match, instantiate, parse_sexpr, format_sexpr
# Create a rewriter function directly
simplify = rewriter(rules, fold_funcs=ARITHMETIC_PRELUDE)
result = simplify(expr)
# Pattern matching
bindings = match(pattern, expr, [])
if bindings != "failed":
result = instantiate(skeleton, bindings, fold_funcs)
# S-expression parsing
expr = parse_sexpr("(+ x (* 2 y))") # => ["+", "x", ["*", 2, "y"]]
text = format_sexpr(expr) # => "(+ x (* 2 y))"
Command-Line Interface
RERUM includes a CLI for interactive use and scripting.
REPL Mode
$ rerum
rerum> @add-zero: (+ ?x 0) => :x
Added 1 rule(s)
rerum> (+ y 0)
y
rerum> :quit
Script Mode
Create a .rerum file:
#!/usr/bin/env rerum
:prelude full
# Symbolic rule: transforms structure, no computation
@square: (square ?x) => (* :x :x)
# Computation rule: (!) evaluates when args are constants
@fold-mul: (* ?a ?b) => (! * :a :b) when (! and (! const? :a) (! const? :b))
# (square x) => (* x x) (symbolic, x stays as x)
(square x)
# (square 5) => (* 5 5) => 25 (fold-mul computes the result)
(square 5)
Output:
(* x x)
25
Run scripts:
$ rerum script.rerum
$ chmod +x script.rerum && ./script.rerum # With shebang
One-Shot Mode
$ rerum -r rules.rules -p full -e "(+ x 0)"
x
Pipe Mode
$ echo "(+ x 0)" | rerum -r rules.rules -p full -q
x
CLI Options
rerum [script] Run a script or start REPL
-r, --rules FILE Load rules (can repeat)
-e, --expr EXPR Evaluate single expression
-p, --prelude NAME Set prelude (arithmetic, math, full, none, or path.py)
-t, --trace Enable tracing
-s, --strategy NAME Strategy: exhaustive, once, bottomup, topdown
-q, --quiet Suppress non-essential output
REPL Commands
:help Show help
:load FILE Load rules from file
:rules List loaded rules
:clear Clear all rules
:prelude NAME Set prelude
:trace on|off Toggle tracing
:strategy NAME Set rewriting strategy
:groups Show groups
:enable GROUP Enable a group
:disable GROUP Disable a group
:quit Exit
Custom Preludes
Create a Python file with a PRELUDE dict:
# my_prelude.py
from rerum import binary_only, unary_only
import math
PRELUDE = {
"gcd": binary_only(math.gcd),
"factorial": unary_only(math.factorial),
}
Use it:
$ rerum -p my_prelude.py -r rules.rules
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rerum-0.2.2.tar.gz.
File metadata
- Download URL: rerum-0.2.2.tar.gz
- Upload date:
- Size: 61.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa836d59e9bfb8afa9b3451ff421d6e47b048f48acc7c9c4e97887bb6660d9ee
|
|
| MD5 |
5120f2cee87c3f287e2e0b0e43147b62
|
|
| BLAKE2b-256 |
1ed5fef85f9f961f92c17a5d20e1bf03856c03b72a1c107c669cc3e179a660af
|
File details
Details for the file rerum-0.2.2-py3-none-any.whl.
File metadata
- Download URL: rerum-0.2.2-py3-none-any.whl
- Upload date:
- Size: 59.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
64d355381c355106ad6a672f722053cc29b8aca809432b48f1a1a6a4ed84b99e
|
|
| MD5 |
13f52eecd1c484085aa0aa640ee65a5b
|
|
| BLAKE2b-256 |
634aa0deb1beca3896735bfe23f5bef6746f6d8440f2c8073b0fe62b123224ce
|