Skip to content

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
(:needsring <x>) ⇐ (<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, in its canonical form, a constant polynomial: a leading zero is discarded, meaning (pos zint <01>) compiles like (pos zint &1); (neg zint &0) compiles to the zero polynomial (pos zint &0)
natural numeral promoted to (pos zint N), with N in its canonical form
(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, cons lists that are not numerals like <x c>, zint terms whose magnitude is not a numeral such as (pos zint x) – gets no result, and the request remains unanswered without notice.

Two consequences worth stating.

Constants are indeterminates. The expression c ~ symconst compiles like a variable: an identity that holds in ℤ[c, x, …] holds for every value of c. Just like any variable, c needs to appear in the pouter ordering. However, the converse does not hold: disproven says that the two sides differ as polynomials in independent indeterminates, not that they differ for the particular value represented by c. A declared equation such as (r * r) = &2 is disregarded both for constants and for variables, but the simplifier’s knowledge folding does consider it: within a single session, :simplify (r * r) answers &2, while (r * r) ≡ &2 results in disproven. A proven verdict stays sound under any such equation, since an identity in the free ring holds in every quotient of that ring.

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)

Here, cancellation operates across entire polynomials. symbolic-minus performs local elimination of X - X when both operands simplify to the same node, so that (:simplify (x - x)) yields &0. Operands that are equivalent only in the polynomial sense are not impacted by this rule; instead, the compiler transforms both into the same canonical form:

zelph+> <x y> ~ polyring
(:needsring <x y>) ⇐ (<x y> ~ polyring)
zelph+> ? :simplify ((x + y) - (y + x))
Answer: (:simplify $( x + y - (y + x) )) = $( x + y - (y + x) )
zelph+> ? :topoly ((x + y) - (y + x))
Answer: (:topoly $( x + y - (y + x) )) = (pos zint &0)

A constant result is a zint term – (pos zint &0) in this instance, rather than the natural &0. topoly generates outputs that reside within the polynomial layer, which keeps (pos zint N) as its coefficient form. Outside that layer, a nonnegative integer is expressed as the natural numeral; (pos zint N) persists as the operand form for z-operations, which likewise generate it (Integers over ℤ).

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 the two sides compiled to the same normal form
disproven both sides were successfully compiled, to distinct normal forms – they differ as polynomials; declared equations are not taken into account
(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.