Primality¶
Modules: stdlib/primes.zph
and stdlib/primes-naf.zph
Β· Prerequisite: an arithmetic substrate
Trial division, twice. The two modules solve the same problem with opposite techniques β a positive fold and negation-as-failure β and the pair is the standard library's worked comparison of the two.
Request idiom¶
zelph> .import primes
zelph+> ? :testprime &97
Answer: (:testprime &97) = prime
zelph+> ? :testprime &91
Answer: (:testprime &91) = composite
zelph+> &91 hasdivisor _D
Answer: &91 hasdivisor &7
| Request | Answer | Silent when |
|---|---|---|
(N testprime N) = X |
prime or composite |
N is &0 or &1 |
N isprime N |
derived for every proven prime | β |
N hasdivisor D |
a divisor witness β see below | N prime |
0 and 1 are neither prime nor composite, and the test derives nothing for them. Partiality by absence, as everywhere in the arithmetic standard library.
The trigger is the self-fact (N testprime N). Entering the query form
directly suffices: parsing it materialises the inner fact as a side effect,
which seeds the whole computation β exactly like (&12 + &34) = X.
primes β the positive fold¶
The statement "N is prime" is universally quantified β all potential divisors yield a non-zero remainder β which might superficially imply using negation-as-failure with hasdivisor. This would be incorrect here. Negation-as-failure evaluates absence within the current graph state, yet hasdivisor facts continue to be derived across multiple fixpoint iterations. Forward chaining never retracts a fact once established: thus, an isprime fact derived too early would stand.
So primality is built from positive facts only. A fold (N nodivupto D)
grows one verified non-divisor at a time:
(N testprime N, &2 divisorcand N, (N mod &2) = R, R != &0) => (N nodivupto &2)
(N nodivupto D, (D + &1) = E, E divisorcand N, (N mod E) = R, R != &0)
=> (N nodivupto E)
The fold is the scheduler. Candidate E = D+1 only comes into existence after D has been verified as a non-divisor. Two consequences:
- For a composite N the search halts at the smallest divisor. No work is
performed past the verdict, and
hasdivisornames exactly one witness β the smallest prime factor. - Candidates stop at EΒ·E > N, so the scan is O(βN) divisions.
The P == N boundary rule is essential: without it, perfect squares like
&9 would pass as prime, because candidate 3 would never be created.
primes-naf β the textbook formulation¶
The same problem, stated the way a textbook states it: N is prime iff no
candidate divides it. This needs
stratified evaluation β the negated
rule is deferred until the positive rules, candidate enumeration and all
mod computations, have reached quiescence, so the negation tests absence
against the complete divisor scan.
.strata lists the rule as not stratifiable nonetheless, together with the
whole arithmetic. The analysis does not compare whole patterns, and the
rule that determines the verdict,
(N testprime N, N isprime N) => ((N testprime N) = prime), creates an =
fact β the predicate for each arithmetic result the scan reads β and
contains (N testprime N), which starts the scan. Neither can
produce a fact that the negation relies upon: the = fact has a
testprime term as its subject, never one of the arithmetic terms the scan
reads, and (N testprime N) is already established, being the ruleβs own
premise. Thus, every hasdivisor fact for N is derived before the negation
is tested, and .semi-naive check verifies this in every run.
It scans eagerly up to βN, because the negation needs the full scan. In return it finds all divisors β€ βN, not just the smallest:
zelph> .import primes-naf
zelph+> ? :testprime &60
Answer: (:testprime &60) = composite
zelph+> &60 hasdivisor _D
Answer: &60 hasdivisor &3
Answer: &60 hasdivisor &6
Answer: &60 hasdivisor &5
Answer: &60 hasdivisor &4
Answer: &60 hasdivisor &2
Choosing between them¶
primes |
primes-naf |
|
|---|---|---|
| Technique | positive fold | negation-as-failure |
| Defers a negation | no | yes |
| Composite N | halts at the smallest divisor | examines up to βN |
hasdivisor |
one witness | every divisor β€ βN |
| Resembles the definition | no | yes |
Neither is the "right" one. primes is the better computation; primes-naf
is the better statement of the mathematics. That both are expressible, in
the same language, over the same arithmetic, is the point of having both in
the standard library.
Node-identity guards¶
The guards R != &0, &2 == N, P == N and the bound comparisons compare nodes using the relational facts from the comparison module. This approach is sound since every number involved is canonical, and canonical numbers are hash-consed: a single value corresponds to a single node, and distinct values produce distinct nodes, barring a hash collision, which zelph refuses (details).
Cross-module cascade¶
No module performs computation independently. The expressions (N mod D), (D + &1), (E * E), and (P cmp N) are ordinary facts asserted for the arithmetic modules to resolve. When .explain is applied to a primes verdict, or to a composite verdict from primes-naf, it therefore traverses the division and multiplication recursions down to the digit level. At this level, the leaves consist of digit-table entries and alphabet facts under decimal-arithmetic and binary-arithmetic, while under binary-nand-arithmetic they include the single NAND axiom, the two alphabet facts, and the [absent] leaves resulting from gate completion. On every substrate, the leaves also include the :testprime N request and structural base cases such as (nil lcmp nil) res eq. A prime verdict from primes-naf does not go through division at all: the scan stands behind its leaf Β¬(N hasdivisor D) [absent].
Testing¶
src/test/test_primes.cpp runs both modules against all three arithmetic
substrates. src/test/test_stratified.cpp covers the scheduling that
primes-naf depends on.