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-syntax loaded: $( ... ) term islands (infix with precedence)
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
(:needsring <x y z>) โ (<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 below 30, loosest first>
factor := '-' factor | power
power := tight ('^' factor)?
tight := <one level per declared precedence above 30, loosest first>
primary := INTEGER | NAME '(' expr ')' | NAME | '(' expr ')'
NAME := IDENT | '"' IDENT '"'
The tightest level of expr reads factors, the tightest level of tight reads primaries; with no operator declared above 30, tight equates to primary. An operand that comes after an operator of tight can also be '-' factor.
| Form | Builds |
|---|---|
INTEGER |
(zelph/number "โฆ"), specifically the &-literal |
IDENT |
a zelph variable when shaped like a variable (single uppercase letter, or prefixed with _), otherwise a named node within the current language |
"IDENT" |
always a named node; this is the way the printer writes a node whose name follows variable shape, like "A" or "_k" |
f(u) |
(f of u) โ only one argument permitted |
-u |
(neg of u); when symbolic-integers is loaded, -3 promotes to (neg zint &3) |
t^u |
(t ^ u), applicable to any factor u: x^y^z becomes (x ^ (y ^ z)), x^-1 becomes (x ^ (neg of &1)). The ^ symbol functions as a term former, not as syntactic sugar for multiplication |
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 precedence levels within the grammar are generated from an operator table that simultaneously supplies the display scheme โ a single source, ensuring the parser and the printer remain synchronized, and guaranteeing that whatever the island writes, it reads back correctly. To extend the notation:
zelph> %(math-syntax/operator "circ" 15)
zelph> %(math-syntax/operator "**" 40 :right)
The built-in operators + - are assigned level 10, * / are at level 20, and ^ is at level 30; all except ^ are left-associative, and :left serves as the default associativity for any newly declared operator. The unary minus operator occupies a position between ^ and the lower levels: it binds more tightly than any level below 30, and less tightly than ^ and any operator declared at a level above 30, meaning that -x^2 is (neg of (x ^ &2)). A sign operator binds the same way when following an operator declared at a level above 30: with ** set at level 40, the expression a ** -b ** c becomes (a "**" (neg of (b "**" c))). An operator declared at level 30 is integrated into the same level as ^ and must be specified as :right.
A name that a statement has to quote is printed within quotation marks inside an island too, and is then read back by the island. For an operator like **, either form is acceptable: $( a "**" b + c ) equates to $( a ** b + c ). For a leaf or a call head, the quotation marks prevent a node named A or _k from being interpreted as a variable upon reading back.
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 produces an island-form representation of a term only when the default rendering would deviate โ specifically, when precedence genuinely eliminates parentheses, or when an application prints in call notation (refer to of below). Numerals keep their &, ensuring a numeral never opens an island by itself. All other elements maintain their ordinary form, which explains why one side of a result frequently appears verbose while the other appears 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.