Symbolic Mathematics
zelph performs symbolic mathematics — term simplification, symbolic
differentiation, and compilation into a single-operator basis — purely inside
its reasoning engine. As with arithmetic, there is no
computer-algebra code in the C++ core: terms are ordinary graph structure,
rewrite rules are ordinary forward-chaining rules, and every simplified form
and every derivative arrives as an ordinary fact carrying its derivation
(⇐). This page delivers the first slice of the symbolic half of the
mathematics-engine roadmap announced in
Semantic Arithmetic.
The modules live in the standard library and stack on top of any arithmetic
module (the import order matters: &-literals in the symbolic scripts are
parsed by the loaded zelph/number at import time):
stdlib/symbolic-core.zph— term sorts, the simplification core, knowledge foldingstdlib/diff.zph— symbolic differentiationstdlib/eml.zph— case study: the EML Sheffer operator
zelph> .import arithmetic # or binary-arithmetic / binary-nand-arithmetic
zelph> .import symbolic-core
zelph> .import diff
zelph> x ~ symvar
zelph> (x * x) diffby x
zelph> ((x * x) diffby x) = D
Answer: ((x * x) diffby x) = (x + x)
The derivative was not computed by a differentiation routine; it was derived,
the same way Berlin is located in Europe follows from a transitivity rule —
and like every derived fact, it persists: repeating the query answers from the
graph.
Terms Are Graph Structure
A symbolic term is nothing new. Binary operations use the same predicates as
the numeric modules — a symbolic (x + y) and a numeric (&2 + &3) are
knowledge about the same + node. Symbolic atoms are named nodes with
declared sorts:
x ~ symvar # variables
c ~ symconst # opaque base constants
Unary functions have no dedicated fact arity in zelph; they are application
facts (exp of x) with the function symbol as a first-class node. This is
the meta-rule pattern from Logic and Computation
transplanted to function symbols: properties of functions are ordinary facts,
exp inverseof ln
ln inverseof exp
and a single generic rule collapses f(f⁻¹(u)) for all declared pairs.
New function symbols work out of the box — undeclared ones simply have no
rewrites.
Numerals — the cons-lists of the loaded arithmetic module — are opaque leaves
here. The symbolic layer is representation-agnostic: its &0/&1 leaves are
whatever the loaded module builds, which is why the test suite runs every
symbolic test against all three arithmetic modules.
Declaring sorts is not optional politeness: an undeclared atom gets no normal form at all, and the simplification request stays silently unanswered. Partiality by absence — silence, never a wrong answer.
The Simplification Core
The user-facing idiom mirrors testprime: a request self-fact seeds the work
— entered with the self-fact prefix : — and
a query retrieves the result repeatably.
zelph> :simplify (x + &0)
zelph> (:simplify (x + &0)) = X
Answer: (:simplify (x + &0)) = x
Internally the module follows the arithmetic architecture, with one new stage:
- Trigger — the request self-fact
(T simplify T), entered as:simplify T, marks the term (needssimp, theneedscanonpattern generalized to terms). - Decompose — markers propagate to all subterms.
- Base cases — declared atoms and numerals are their own normal forms.
- Congruence — once the children are normal, the context-reduced form is
built:
(T red (P + Q)). Thanks to hash-consing, a term whose children were already normal reduces to itself — a self-fact(T red T). - Rewrite — directed rules on reduced forms produce
(T rw S): neutral and absorbing elements, the generic inverse-pair rule, knowledge folding (below), and any operator extensions such aseml.zph. - Connect — the rewrite result if one exists, otherwise the reduced form
itself; exposed under
=.
Two design decisions deserve emphasis.
No free commutativity or associativity. A monotonic engine never deletes:
freely commuting rules would double every term, and associativity plus
congruence would blow up the term space. Instead, every rewrite rule is
directed and measure-reducing, both orientations of symmetric identities are
spelled out explicitly (X + &0 and &0 + X), and the whole machinery is
gated by markers so it never touches numeric facts. Equality saturation in
the e-graph style — which is, at heart, forward chaining over equalities — is
a natural future experiment, not the foundation.
The normal-form contract. Every rewrite right-hand side must already be a
normal form — a leaf, or built from normal children of the reduced form. All
shipped rules satisfy this, which is why one bottom-up pass suffices and
simp is single-valued. Rules violating the contract (distributivity, say)
need an iterated design and must not be added casually.
The module's only negation is the identity fallback: a term whose reduced form has no rewrite is its own normal form,
(T red C, ¬(T rw S)) => (T simp C)
deferred by stratified evaluation until the
positive rules — including the entire numeric cascade feeding knowledge
folding — have reached quiescence. Fallback results feed parent congruences,
which re-open the positive stratum; the alternating schedule makes chains
like exp(ln(x + 0)) → x sound across multiple deferred rounds.
Knowledge Folding
Constant folding needs no dedicated machinery, because the reduced form
is an ordinary +/*// fact: the arithmetic module's triggers fire on
it automatically, derive its = result, and one bridge rule adopts it:
(T red C, C = R) => (T rw R)
zelph> :simplify ((&2 + &3) * (&4 + &6))
zelph> (:simplify ((&2 + &3) * (&4 + &6))) = X
Answer: (:simplify ((&2 + &3) * (&4 + &6))) = &50
The inner sums fold to &5 and &10; congruence then materializes the fresh
fact (&5 * &10) mid-simplification, whose numeric cascade the bridge
consumes — the same cross-module mechanism by which
multiplication delegates to addition.
The rule is deliberately more general than constant folding: it consumes any equational fact about the reduced form — computed by the arithmetic modules, or declared. An equation imported from a knowledge graph drives simplification exactly like a computed one; knowledge and computation remain one substrate. (Declared equations must respect the normal-form contract on their right-hand side, and conflicting declarations would break single-valuedness.)
Confluence with the plain rewrites holds on every overlap because numeric
results of canonical operands are canonical — addition and multiplication by
construction, division via canonnum. And partiality composes: &5 / &0
derives no = fact, matches no rewrite, and falls back to itself —
undefinedness stays visible instead of folding to a wrong value.
Symbolic Differentiation
stdlib/diff.zph
implements sum, product, and chain rule as Trigger / Decompose / Assemble /
Connect blocks. Function derivatives are again facts about function
symbols (exp hasderivative exp), consumed by one generic chain rule; ln
has a dedicated rule because its derivative 1/u is not of the form g(u)
for a named symbol.
Constancy is the textbook definition, executable: a positive containment
recursion plus negation-as-failure — the
primes-naf
pattern:
(T dstate X, X ~ symvar, ¬(T contains X)) => ((T wrt X) deriv &0)
Numerals need no special rule: nothing derives (&n contains X), so they are
constants by absence. Constant composites are reached by two derivation
paths — the NAF short-circuit and the structural rules over &0-children —
and both collapse to the same &0 through the simplifier, keeping the
exposed result single-valued.
That pipeline is the module's connect stage: the raw derivative is not the
exposed result. It is pushed through an ordinary :simplify D request —
another cross-module cascade — so d(x + c)/dx answers &1 (not
&1 + &0), and d(x + x)/dx answers &2 (the raw &1 + &1 folded through
the numeric module):
zelph> (x + c) diffby x
zelph> ((x + c) diffby x) = D
Answer: ((x + c) diffby x) = &1
zelph> ((ln of x) diffby x) = D
Answer: ((ln of x) diffby x) = (&1 / x)
Case Study: The EML Sheffer Operator
In All elementary functions from a single binary operator
(arXiv:2603.21852), Odrzywołek shows that
the single operator eml(x, y) = exp(x) − ln(y), together with the constant
1, generates the entire repertoire of a scientific calculator — a Sheffer
stroke for continuous mathematics, as NAND is for Boolean logic. (The NAND
analogy is taken literally elsewhere in the standard library:
binary-nand-arithmetic.zph
derives all digit tables of binary arithmetic from a single NAND axiom.)
stdlib/eml.zph
makes eml a first-class symbolic operator via the operator extension
protocol — the three contributions any new operator makes to the
simplifier: decompose rules, a congruence rule, and rewrite rules on reduced
forms. The connect stage and the identity fallback work unchanged.
The rewrite rules are an identity table taken from the paper: they
eliminate eml in favor of named functions. The flagship is Eq. (5),
ln z = eml(1, eml(eml(1, z), 1)) — and zelph derives it:
zelph> .import eml
zelph> x ~ symvar
zelph> :simplify (&1 eml ((&1 eml x) eml &1))
zelph> (:simplify (&1 eml ((&1 eml x) eml &1))) = X
Answer: (:simplify (&1 eml ((&1 eml x) eml &1))) = (ln of x)
The identity is not checked numerically and not assumed: it is derived, as
a chain of ordinary deductions by the same fixpoint engine that reasons over
Wikidata — with the full ⇐ provenance attached.
Identity patterns match normalized forms. A subtlety worth recording for
future identity-table authors: because simplification is bottom-up, the
literal Eq.-(5) tree never reaches the top-level rewrite stage intact — its
inner (… eml &1) has already been reduced to (exp of …) by the exp rule.
Table patterns must therefore be stated over bottom-up-normalized forms; the
ln rule recognizes (&1 eml (exp of (&1 eml X))), matched as a single deep
condition by deep unification.
Single-pass semantics. Rewrite results are not re-processed within a
request, so e = eml(1, 1) reduces in two requests: (&1 eml &1) answers
(exp of &1), and simplifying that answers e. simp stays
single-valued throughout; one-shot deep normalization is the future
iterated/e-graph design.
The compiler. The expansion direction, :emlcompile T, builds pure-EML
forms bottom-up under the same architecture and answers the repeatable query
(:emlcompile T) = F. Compile-and-reduce round trips close for exp and
ln:
zelph> :emlcompile (ln of x)
zelph> (:emlcompile (ln of x)) = F
Answer: (:emlcompile (ln of x)) = (&1 eml ((&1 eml x) eml &1))
Coverage is deliberately strict — exp, ln, eml itself, and leaves;
anything else yields no emlform rather than a form falsely claimed to be
pure EML. Extending it along the paper's verified discovery chain
(Supplementary Information, Table S2) is mechanical per-constructor work and
is the module's roadmap. A structural bonus falls out of the engine: since
all terms are hash-consed, EML trees are canonical DAGs — repeated
subexpressions are stored, matched, and rewritten once.
Coexistence with the Numeric Substrate
Sharing predicates has a price, paid deliberately: the numeric triggers fire
on symbolic facts and create dead internal states (((x add y) ci 0)), which
are harmless — digit recursion cannot decompose atoms — and small. In return,
the derivative scaffolding of diff.zph consists of real +/* facts whose
numeric results appear in the same graph, which is what makes knowledge
folding a one-rule bridge instead of an integration layer.
Scope and Honest Limitations
Identities are formal: exp inverseof ln assumes the principal branch
and u > 0 over the reals, &0 / X ignores division by zero; side
conditions are not tracked yet. Knowledge folding computes over the naturals
(the current numeric substrate). Rewriting is single-pass per request, and
the operator set is young — subtraction, negation, and powers are next on the
list, driven by the EML compiler extension.
Testing
src/test/test_symbolic.cpp runs every case against all three arithmetic
modules and both parallelism modes, permanently in .semi-naive check mode —
so the stratified schedule of the identity fallback is continuously verified
against classic evaluation. Assertions are structural (zelph/exists probes
on graph nodes), not string comparisons: symbolic results are checked as the
nodes they are.