The math Front End¶
Modules: stdlib/math.zph
and stdlib/math-syntax.zph
Two small modules that make the rest of the mathematical standard library
usable in three lines. math is the single import and the ring
declaration; math-syntax is the infix notation.
math โ one import and one declaration¶
zelph> .import math
math loaded: declare indeterminates with <x y z> ~ polyring
pulls in topoly, math-syntax,
symbolic-core, symbolic-minus, symbolic-pow,
symbolic-integers and diff โ and, through
them, an arithmetic substrate, integer-arithmetic and
polynomial.
The ring declaration¶
zelph> <x y z> ~ polyring
The subject is a cons list of the ring's indeterminates, outermost first. Four rules turn it into the declarations the polynomial layer consumes:
(L ~ polyring) => (L needsring L) Trigger
((A cons R) needsring (A cons R), R != nil) => (R needsring R) Decompose
((A cons R) needsring (A cons R)) => (A ~ symvar) Sorts
((A cons (B cons S)) needsring (A cons (B cons S)))
=> (A pouter B) Order
so every element becomes an indeterminate and every adjacent pair fixes
the nesting order. Transitivity is already provided by
polynomial, so adjacent pairs suffice:
zelph> %(string "ADJ-" (and (zelph/exists "x" "pouter" "y") (zelph/exists "y" "pouter" "z")))
"ADJ-true"
zelph> %(string "TRANS-" (zelph/exists "x" "pouter" "z"))
"TRANS-true"
zelph> %(string "DIR-" (zelph/exists "y" "pouter" "x"))
"DIR-false"
The order is directional; the reverse is not derivable.
Constants that should stay opaque are declared as usual with ~ symconst
and do not belong in the list โ though note that
topoly treats them as indeterminates too, so they
still need a place in the pouter order.
The decompose rule is guarded with R != nil for the same reason as
elsewhere in the standard library: no marker state is ever created on
nil, which is the graph's biggest hub.
Why a list and not a Janet helper¶
The declaration is an ordinary fact about an ordinary node, so it is
visible to inference like everything else. Rules can quantify over rings,
further facts can attach to the same list node, and it survives .save. A
Janet helper would have produced the same facts and left nothing to reason
about โ which would have contradicted the point of the system.
Notation¶
math-syntax registers the $( โฆ ) term island: conventional infix
notation inside a statement that is otherwise ordinary zelph.
$( x^2 + 2*x + 1 ) diffby x
(T red $( X * 1 )) => (T rw X) # node-identical to (X * &1)
An island desugars to exactly the graph structure the verbose syntax builds. Hash-consing makes both spellings meet at identical nodes, so they are freely mixable โ in facts and in rules.
Grammar¶
expr := <one level per declared precedence, loosest first>
factor := '-' factor | power
power := primary ('^' INTEGER)?
primary := INTEGER | IDENT '(' expr ')' | IDENT | '(' expr ')'
| Form | Builds |
|---|---|
INTEGER |
(zelph/number "โฆ"), i.e. the &-literal |
IDENT |
a zelph variable if variable-shaped (single uppercase letter, or leading _), otherwise a named node in the current language |
f(u) |
(f of u) โ single argument only |
-u |
(neg of u); with symbolic-integers loaded, -3 promotes to (neg zint &3) |
t^n |
(t ^ &n), n โฅ 0 an integer literal. ^ is a term former, not sugar for a product |
Identifiers are [A-Za-z_][A-Za-z0-9_]*; atoms outside that charset need
the verbose syntax. Deliberate omissions: no implicit multiplication (2x
is an error, write 2*x), no unquote inside islands, no comparison
operators, no string literals, no user-defined prefix or postfix operators.
Note that x^2 and x*x are different nodes. Their equality is a
statement of the polynomial layer, not an assumption of the parser.
Adding an operator¶
The infix levels of the grammar are generated from an operator table that also feeds the display scheme โ one table, so the parser and the printer cannot drift apart. To extend the notation:
zelph> %(math-syntax/operator "circ" 15)
zelph> %(math-syntax/operator "**" 40 :right)
The built-ins are + - at 10, * / at 20, ^ at 30. Associativity
defaults to :left.
Word-shaped operator names are matched with an identifier boundary, so
circ never matches inside circle; they need surrounding whitespace,
symbolic ones do not. Operators sharing a precedence must share an
associativity โ mixing them is an error rather than a silent choice โ and a
rejected table leaves neither the grammar nor the display registry changed.
Registering an operator for display only, with zelph/set-infix-display
on the math-syntax scheme, is possible but ill-advised: zelph would then
print island syntax its own parser refuses to read.
Display¶
The scheme renders a term in island form only where the default rendering would deviate โ where precedence actually removes parentheses, or where a numeral drops its sigil. Everything else keeps its ordinary form, which is why one side of an answer often prints verbosely and the other as an island.
of is registered in application form, so (f of u) reads back as
f(u). That also covers unary minus: $( -x ) builds (neg of x) and
renders as neg(x), which this grammar parses. = stays unregistered โ it
belongs to the arithmetic modules and has no place in this grammar, so a
result fact keeps = outside the island.
Mechanics¶
The island is an inline keyword โ the three-argument form of
zelph/register-keyword. The host's close-delimiter scan is raw, so the
handler arbitrates nested ) via an :incomplete veto keyed on
parenthesis balance. Per the inline-keyword contract the handler is
side-effect-free until it accepts: the PEG parse runs first, graph
construction only afterwards. Balanced but unparsable content is an
error, never :incomplete โ a veto there would swallow the surrounding
statement text.
Testing¶
src/test/test_math.cpp covers the ring declaration;
src/test/test_math_syntax.cpp covers the grammar, precedence,
associativity, the operator extension and its error paths, across all three
arithmetic substrates.