Below you will find pages that utilize the taxonomy term “Rerum”
Building Languages to Solve Problems
January 19, 2026
Chapter 4 of Structure and Interpretation of Computer Programs opens with one of the most important insights in programming: the most powerful technique for controlling complexity is metalinguistic abstraction, the establishment of new languages.
Not libraries. Not frameworks. Languages.
When you’ve abstracted enough of a problem domain into primitives, combination rules, and naming mechanisms, you haven’t just written code. You’ve created a new way of thinking about the problem. The domain becomes expressible. And once something is expressible, it becomes manipulable, debuggable, and shareable.
What Is Metalinguistic Abstraction?
The key distinction is between using a language and creating one. A library gives you functions to call. A language gives you a grammar for expressing ideas.
Consider the difference:
Library approach: Call db.execute("SELECT * FROM users WHERE age > 21")
Language approach: Write SELECT * FROM users WHERE age > 21
SQL isn’t a library. It’s a language, with primitives (tables, columns), means of combination (joins, unions, subqueries), and means of abstraction (views, CTEs). These three elements (primitives, combination, abstraction) are SICP’s fundamental criteria for any language, and they’re what separates a DSL from a mere API.
Other examples:
- Regular expressions: primitives (characters, character classes), combination (concatenation, alternation), abstraction (groups, backreferences)
- Make: primitives (targets, prerequisites), combination (dependency chains), abstraction (pattern rules, variables)
- CSS selectors: primitives (elements, classes, IDs), combination (descendant, child, sibling), abstraction (custom properties, mixins in preprocessors)
In each case, the language captures the essential structure of the problem domain in a way that raw code cannot.
The Three Requirements
SICP identifies three necessary components for any language:
- Primitives: What are the basic elements that cannot be broken down further?
- Means of combination: How do you build compound elements from simpler ones?
- Means of abstraction: How do you name and reuse patterns?
When designing a DSL, these questions guide everything. Get them wrong and you have a clunky API. Get them right and the domain becomes thinkable in your language.
Consider an expression language for symbolic math:
- Primitives: numbers, symbols, operators
- Combination: function application
(+ x 1), nested expressions(* (+ x 1) 2) - Abstraction: named rules, rulesets, engines
Or a query language for JSON documents:
Rerum: Pattern Matching and Term Rewriting in Python
December 16, 2025
Rerum (Rewriting Expressions via Rules Using Morphisms) is a Python library for pattern matching and term rewriting. It makes symbolic computation accessible through a readable DSL while keeping a clean separation between trusted and untrusted code.
The Problem
Traditional symbolic math systems tend toward two extremes. Monolithic systems like Mathematica bundle everything in. Lighter tools force you to write complex recursive traversals every time you want to transform an expression. I wanted something in between: a simple, extensible system where transformation rules are data that can be loaded, combined, and inspected.
The SICP Connection
This design reflects a core idea from Structure and Interpretation of Computer Programs: when a problem domain is complex enough, the right move is to build a language for it. Rerum’s rule DSL makes transformation logic inspectable, composable, and safe.
The engine composition operators (>> for sequencing, | for union) ensure closure: combining engines yields an engine. Same principle that makes Scheme’s procedures powerful. You can pass them, return them, combine them, no special cases. Transformation strategies are first-class.
A Readable DSL
At the heart of rerum is a domain-specific language for defining rewrite rules:
# Algebraic simplification
@add-zero[100] "x + 0 = x": (+ ?x 0) => :x
@mul-one[100]: (* ?x 1) => :x
@mul-zero[100]: (* ?x 0) => 0
Each rule has:
- A name:
@add-zerofor debugging and tracing - Optional priority:
[100]determines firing order when multiple rules match - Optional description: Human-readable explanation
- A pattern:
(+ ?x 0)matches addition with zero - A skeleton:
:xis the replacement
The pattern syntax:
| Syntax | Meaning |
|---|---|
?x | Match anything, bind to x |
?x:const | Match only numbers |
?x:var | Match only symbols |
?x:free(v) | Match expressions not containing v |
?x... | Variadic, capture remaining arguments |
Symbolic Differentiation in 15 Lines
Here’s a calculus ruleset that computes symbolic derivatives:
[basic-derivatives]
@dd-const[100]: (dd ?c:const ?v:var) => 0
@dd-var-same[100]: (dd ?x:var ?x) => 1
@dd-var-diff[90]: (dd ?y:var ?x:var) => 0
[rules]
@dd-sum: (dd (+ ?f ?g) ?v:var) => (+ (dd :f :v) (dd :g :v))
@dd-product: (dd (* ?f ?g) ?v:var) => (+ (* (dd :f :v) :g) (* :f (dd :g :v)))
@dd-power: (dd (^ ?f ?n:const) ?v:var) => (* :n (* (^ :f (- :n 1)) (dd :f :v)))
@dd-exp: (dd (exp ?f) ?v:var) => (* (exp :f) (dd :f :v))
@dd-log: (dd (ln ?f) ?v:var) => (/ (dd :f :v) :f)
@dd-sin: (dd (sin ?f) ?v:var) => (* (cos :f) (dd :f :v))
@dd-cos: (dd (cos ?f) ?v:var) => (* (- (sin :f)) (dd :f :v))
With these rules loaded:
from rerum import RuleEngine, E
engine = RuleEngine.from_file("calculus.rules")
# d/dx(x^2) = 2x
engine(E("(dd (^ x 2) x)")) # => (* 2 (* (^ x 1) 1))
The result needs simplification (another ruleset), but the differentiation itself is purely declarative.
The Security Model: Rules vs. Preludes
A key architectural decision: the separation between rules (untrusted, serializable) and preludes (trusted Python code). Rules define structural transformations. They can reference operations via the (! op args...) compute form, but those operations must be explicitly provided by the host.
symlik: Symbolic Likelihood Models in Python
December 16, 2025
symlik is a Python library for symbolic likelihood models. Write your log-likelihood as a symbolic expression, and it derives everything needed for inference.
The Problem
Traditional statistical computing gives you two choices:
- Manual derivation. Work out score functions and information matrices by hand, then implement them. Error-prone, tedious.
- Numerical approximation. Use finite differences. Unstable, slow, no symbolic form to inspect.
The Approach
symlik takes a third path: symbolic differentiation. Define the model once, get exact derivatives automatically.
from symlik.distributions import exponential
model = exponential()
data = {'x': [1.2, 0.8, 2.1, 1.5]}
mle, _ = model.mle(data=data, init={'lambda': 1.0})
se = model.se(mle, data)
print(f"Rate: {mle['lambda']:.3f} +/- {se['lambda']:.3f}")
# Rate: 0.714 +/- 0.357
Behind the scenes, symlik:
- Symbolically differentiates the log-likelihood to get the score function
- Differentiates again for the Hessian
- Computes Fisher information from the Hessian
- Derives standard errors from the inverse information matrix
All exact. No numerical approximation.
Custom Models
The real power is defining custom models using s-expressions:
from symlik import LikelihoodModel
# Exponential: l(lambda) = sum[log(lambda) - lambda*x_i]
log_lik = ['sum', 'i', ['len', 'x'],
['+', ['log', 'lambda'],
['*', -1, ['*', 'lambda', ['@', 'x', 'i']]]]]
model = LikelihoodModel(log_lik, params=['lambda'])
# Symbolic derivatives available
score = model.score() # Gradient
hess = model.hessian() # Hessian matrix
info = model.information() # Fisher information
You define the log-likelihood once as a symbolic expression. symlik computes the rest.
Heterogeneous Data
One of symlik’s strengths is handling mixed observation types, which is exactly what you need for reliability analysis with censored data:
from symlik import ContributionModel
from symlik.contributions import complete_exponential, right_censored_exponential
model = ContributionModel(
params=["lambda"],
type_column="status",
contributions={
"observed": complete_exponential(),
"censored": right_censored_exponential(),
}
)
data = {
"status": ["observed", "censored", "observed", "observed", "censored"],
"t": [1.2, 3.0, 0.8, 2.1, 4.5],
}
Each observation type contributes differently to the likelihood. symlik handles the bookkeeping.
Connection to Research
symlik is the Python successor to my R package likelihood.model. It implements the theoretical framework from my thesis work on likelihood-based inference for series systems.
The Weibull Series Model Selection paper shows applications to reliability engineering, the kind of complex likelihood that benefits from symbolic treatment.
Powered by rerum
symlik uses rerum for symbolic differentiation. rerum is a pattern matching and term rewriting library that handles the calculus. The separation means you can use rerum for other symbolic computation tasks beyond likelihood models.
Installation
Available on PyPI:
pip install symlik
Documentation at queelius.github.io/symlik.
See the project page for more details.