Skip to content

Integers over ℤ

Module: stdlib/integer-arithmetic.zph · Prerequisite: an arithmetic substrate (a default is imported if you do not choose one)

Signed integers on top of the natural-number modules. The module owns no recursion of its own: every rule keys on a user-facing fact and delegates the magnitude work to the naturals by asserting ordinary +, -, * and cmp facts. It is therefore base-agnostic and runs unchanged on all three substrates.

Representation

An integer is the ordinary term

(pos zint N)        (neg zint N)

with N a natural-number cons list and pos/neg plain atoms. No new machinery — the term is a fact node like any other, built by fact() and matched by deep unification.

Canonical form. Zero is (pos zint &0). The term (neg zint &0) must never be produced: one value, one node, the invariant the naturals pin with canonnum. All rules yield canonical results for canonical operands; pos magnitudes may be any canonical natural, neg magnitudes must be nonzero.

A Janet input helper is provided for signed literals:

zelph> .import integer-arithmetic
zelph> %(print (zelph/int "-0"))
pos zint &0

zelph/int is defined by the module, not by the engine, thus the import is responsible for placing it there. It delegates the magnitude to zelph/number, so a -0 literal canonicalises. It is input convenience only — the same node can be built with zelph/fact or typed verbosely.

Operations

Results are exposed under =, the uniform query idiom.

Request Meaning
(X z+ Y) = Z addition
(X z- Y) = Z subtraction — total on ℤ, unlike natural -
(X zx Y) = Z multiplication
(X zcmp Y) = Z comparison; lt, gt, eq
zelph> .import integer-arithmetic
zelph> ? (pos zint &7) z+ (neg zint &10)
Answer: ((pos zint &7) z+ (neg zint &10)) = (neg zint &3)
zelph> ? (pos zint &7) z- (neg zint &10)
Answer: ((pos zint &7) z- (neg zint &10)) = (pos zint &17)
zelph> ? (neg zint &7) zx (neg zint &6)
Answer: ((neg zint &7) zx (neg zint &6)) = (pos zint &42)
zelph> ? (neg zint &7) zcmp (neg zint &6)
Answer: ((neg zint &7) zcmp (neg zint &6)) = lt

zx rather than z*, for the same reason the digit table is dx: * is parser-reserved inside atom names.

Comparison additionally derives the relational facts <, >, == — the same predicates the naturals use. A meta-rule quantifying over them ("> is transitive") therefore spans ℕ and ℤ without knowing that either exists.

How partiality composes

The design worth studying is mixed-sign addition. Both candidate differences are asserted; natural subtraction silently kills the invalid one; the cmp guards select the matching connect rule:

((pos zint A) z+ (neg zint B)) => (A cmp B)
((pos zint A) z+ (neg zint B)) => (A - B)
((pos zint A) z+ (neg zint B)) => (B - A)

((pos zint A) z+ (neg zint B), A > B, (A - B) = D) => (… = (pos zint D))
((pos zint A) z+ (neg zint B), A == B)             => (… = (pos zint &0))
((pos zint A) z+ (neg zint B), A < B, (B - A) = D) => (… = (neg zint D))

Nothing tests which branch is "valid". The invalid subtraction simply derives nothing, and the rule that would have consumed it never fires.

Zero guards follow the same principle. Negating a positive subtrahend is guarded by B > &0 so that (neg zint &0) is never materialised, not even as an operand; subtracting zero has its own direct rule:

zelph> ? (pos zint &0) z- (pos zint &0)
Answer: ((pos zint &0) z- (pos zint &0)) = (pos zint &0)

The uniform operator façade

Rules gated on zint-shaped operands route +, -, * and cmp to their z-counterparts and re-expose the results under the natural predicate:

((G zint A) + (H zint B)) => ((G zint A) z+ (H zint B))
((G zint A) + (H zint B), ((G zint A) z+ (H zint B)) = R)
=> (((G zint A) + (H zint B)) = R)

so the query idiom is the same over ℕ and ℤ:

zelph> ? (pos zint &2) + (neg zint &5)
Answer: ((pos zint &2) + (neg zint &5)) = (neg zint &3)

The payoff is elsewhere: symbolic-core's knowledge-folding bridge (T red C, C = R) => (T rw R) consumes these = facts with no ℤ-specific rule at all. Constant folding over ℤ is this façade plus a rule that was already there.

The price is that the naturals' triggers also fire on these facts and create dead internal states such as ((zintA add zintB) ci 0). They are harmless — the digit recursion cannot decompose a fact whose predicate is zint rather than cons — and are the cost of shared predicates.

Division and mod are deliberately not routed. Euclidean division on ℤ is a genuine design choice (floor versus truncation), not an oversight; a zint / fact derives nothing until that choice is made.

Completing natural partiality

Natural subtraction is partial, and loading this module does not change that — it adds a second, disjoint set of rules:

zelph> ? &3 - &5
zelph>
zelph> ? (pos zint &3) - (pos zint &5)
Answer: ((pos zint &3) - (pos zint &5)) = (neg zint &2)

The bridge that lets a term containing natural numerals fall through to ℤ lives one layer up, in symbolic-integers.

Testing

src/test/test_integers.cpp runs the operation matrix against all three arithmetic substrates and both parallelism modes, with structural probes rather than string comparisons.