Term-to-Polynomial Compiler¶
Module: stdlib/topoly.zph
· Prerequisite: polynomial
Compiles symbolic terms to the canonical normal forms of the polynomial layer, and decides polynomial identities on the result. Pure forward-chaining rules, in the standard Trigger / Decompose / Assemble / Connect shape.
symbolic-core is deliberately not a prerequisite. This
module consumes only the shared term vocabulary — the binary operators,
the application (neg of U), sort declarations and numerals — all of which
are ordinary facts that exist with or without the simplifier. The simplifier
and diff are the typical producers of the terms being compiled, not a
dependency.
Request idiom¶
(:topoly T) = P
repeatable, like every result query in the standard library.
zelph> .import math
zelph> <x> ~ polyring
zelph> ? :topoly $( (x+1)^2 )
Answer: (:topoly ((x + &1) ^ &2)) = (x poly <(pos zint &1) (pos zint &2) (pos zint &1)>)
Vocabulary¶
| Term | Compiles to |
|---|---|
X ~ symvar |
the polynomial X: (X poly <(pos zint &0) (pos zint &1)>) |
C ~ symconst |
the same — an opaque constant is an indeterminate here |
| zint numeral | itself, a constant polynomial |
| natural numeral | promoted to (pos zint N) |
(U + V), (U - V), (U * V) |
padd / psub / pmul |
(U ^ N), N a natural numeral |
ppow |
(neg of U) |
pneg |
Anything else — division, other function applications, undeclared atoms — gets no result, and the request stays silently unanswered.
Two consequences worth stating.
Constants are indeterminates. c ~ symconst compiles like a variable,
because an identity that holds in ℤ[c, x, …] holds for every value of c.
Like any variable, c must appear in the pouter order.
Promotion makes subtraction total. Natural subtraction is partial, but here the operands are promoted to ℤ first:
zelph> ? :topoly (&3 - &5)
Answer: (:topoly (&3 - &5)) = (neg zint &2)
This also closes the loop that
symbolic-minus deliberately left
open: (:topoly (x - x)) is (pos zint &0). Cancelling equal symbolic
terms is the polynomial layer's job, and here it happens.
Architecture¶
(T topoly T) => (T needstopoly T) Trigger
((U + V) needstopoly (U + V)) => (U needstopoly U) Decompose
…
(X needstopoly X, X ~ symvar) => (X aspoly (X poly …)) Base
((U + V) needstopoly (U + V), U aspoly P, V aspoly Q)
=> (P padd Q) Assemble (seed)
((U + V) needstopoly (U + V), U aspoly P, V aspoly Q,
(P padd Q) = R) => ((U + V) aspoly R) Assemble (adopt)
(T topoly T, T aspoly P) => ((T topoly T) = P) Connect
The assemble stage is the cross-module cascade used throughout the standard
library: seed the operation as an ordinary fact, let the data layer answer
it, adopt the = result.
Canonicity needs no separate argument: leaves compile to canonical
polynomials, and the delegated operations preserve canonicity, so every
aspoly result is canonical. = is single-valued because each term shape
is matched by exactly one assemble family.
Display sugar needs no registration here. (T topoly T) is a genuine
request, needstopoly a genuine marker, and (P aspoly P) for a leaf
genuinely reads "P is its own polynomial form".
Identity checking¶
Canonical forms are unique hash-consed nodes, so proving a polynomial identity is comparing node identity — expressed through unification:
(A ≡ B) => (:topoly A)
(A ≡ B) => (:topoly B)
(A ≡ B, (:topoly A) = P, (:topoly B) = P) => ((A ≡ B) = proven)
(A ≡ B, (:topoly A) = P, (:topoly B) = Q, P != Q) => ((A ≡ B) = disproven)
The same variable P in both proof conditions binds only if both
compilations reached the identical node. There is no equality checker.
Three outcomes¶
| Answer | Means |
|---|---|
proven |
both sides compiled to the same normal form |
disproven |
both sides compiled, to different normal forms |
| (no answer) | at least one side did not compile |
zelph> ? $( (x+1)^2 ) ≡ $( x^2 + 2*x + 1 )
Answer: … = proven
zelph> ? $( (x+1)^2 ) ≡ $( x^2 + 1 )
Answer: … = disproven
The third case is not a weakness to be engineered away — it is the honest
reading. An undeclared atom, a missing pouter pair, or an operator
outside the vocabulary means the question was never posed to the polynomial
layer, and "I could not compile this" is not "these differ". Ask for the
normal forms directly (? :topoly A) to see which side is missing.
Note also what disproven does not need: no negation-as-failure, no
dependence on the order in which the compile state saturates. Once both
normal forms exist, != decides in the positive stratum.
Extending the vocabulary¶
An operator the compiler does not know can be taught by delegation —
seed its defining term and adopt whatever normal form that reaches. Two
rules suffice; the terms tutorial
walks through an example, and eml applies the same pattern to a
whole macro chain.
Testing¶
src/test/test_topoly.cpp runs against all three arithmetic substrates,
with structural probes. symbolic-core is deliberately not imported there:
the compiler must work from the vocabulary alone.