Core Concepts¶
Semantic Network Structure¶
In zelph, knowledge is represented as a network of nodes connected by relations. Unlike traditional semantic networks where relations are labeled edges, zelph treats relation types as first-class nodes themselves. This unique approach enables powerful meta-reasoning about relations.
Predefined Core Nodes¶
zelph initializes with a small set of fundamental nodes that define the ontology of the system. These nodes are available in every language setting (though their names can be localized).
| Core Node | Symbol | Internal Name | Description |
|---|---|---|---|
| RelationTypeCategory | -> |
RelationTypeCategory |
The meta-category of all relations. Every relation predicate in zelph is an instance (~) of this node. |
| IsA | ~ |
IsA |
The fundamental categorical relation. Used for classification, e.g. to classify a Set as a Conjunction. |
| Causes | => |
Causes |
Defines inference rules. Connects a condition set to a consequence. |
| PartOf | in |
PartOf |
Defines membership in Sets. |
| Cons | cons |
Cons |
The fundamental list-building relation (Lisp-style). The subject is the first element (car), the object is the rest of the list (cdr). |
| Nil | nil |
Nil |
The empty list terminator. Marks the end of a cons-list. |
| Conjunction | conjunction |
Conjunction |
A tag used to mark a Set as a logical AND condition for rules. |
| Unequal | != |
Unequal |
Used to define constraints (e.g., X != Y) within rules. |
| Negation | negation |
Negation |
Marks a rule condition that is negated: ((P) ~ negation) represents the condition that matches when no fact matches the pattern P. |
| Contradiction | ! |
Contradiction |
The result of a rule that detects a logical inconsistency. |
These nodes are the "axioms" of zelph's graph. For example, ~ is defined as an instance of -> (i.e., "IsA" is a "Relation Type"). This self-referential bootstrapping allows zelph to reason about its own structure.
Homoiconicity: The Executable Graph¶
A defining characteristic of zelph is its homoiconicity: logic (code) and facts (data) share the exact same representation. Rules are not separate scripts; they are topological structures within the graph. Math is not hard-coded; numbers are cons-lists of digit nodes that interact with semantic entities through the same rule mechanism.
This means the graph doesn't just describe knowledge; it structures the execution of logic. For a detailed exploration of this concept and its implications for computation, see Logic and Computation.
Facts and Relations¶
A fact in zelph is a statement node created from a subject, a predicate, and an object:
subject predicate object
A predefined predicate is ~, used for classification:
X ~ Y
This can be read as "X is an instance of class Y", but depends on the context of your dataset. It is used in zelph's internal topology — there is no need to actually use it in your scripts.
Working with Custom Relations¶
zelph can use any predicate node, not just ~:
zelph> bright "is opposite of" dark
bright "is opposite of" dark
In this example, using the interactive REPL, we enter a subject-predicate-object triple. Neither "bright", "dark" nor "is opposite of" is know to zelph prior this command. It automatically creates the appropiate nodes and edges in the semantic network. After doing so, in the second line this topology is parsed and printed to verify the process ran as expected.
Note that if a relation contains spaces, it needs to be wrapped in quotation marks, just as a name starting with # must be enclosed in quotation marks to avoid initiating a comment.
Predicates are completely generic: symbolic predicates such as .., -->, or <= are treated in the same way as word-like predicates such as followed-by or is capital of.
Nested Expressions and Sets¶
zelph supports advanced grouping and recursion using parentheses (), braces {}, and angle brackets <>, plus a shorthand prefix : for self-facts.
Parentheses: Nested Facts¶
A parenthesised statement (s p o) creates the fact and evaluates to the statement node (i.e. the relation/fact node). This lets you make statements about statements:
(bright "is opposite of" dark) "is a" "symmetric relation"
Here, the subject of the outer statement is the node representing the inner fact.
The outer statement asserts the inner fact as well: a statement claims itself and every fact nested in it, also those in a set constant, a collection literal, or a comma list it writes, excluding what stands in an opaque position, of which => is one: what resides within a rule it mentions (see Mentioning a Rule Is Not Asserting It), and what an argument of a predicate declared opaque holds (see Mentioning a Statement: Opaque Predicates). The order in which the two were written is irrelevant, so a rule that names the inner fact as its condition fires once a statement holds it:
zelph> (Berlin is-capital-of Germany) => (Germany has-capital yes)
zelph> atlas lists (Berlin is-capital-of Germany)
zelph+> X has-capital Y
Answer: Germany has-capital yes
A comma list lacking variables, when placed on a separate line, asserts every one of its members, thereby claiming each one together with what it nests, just as the line of each individual member does:
zelph> (Berlin is-capital-of Germany) => (Germany has-capital yes)
zelph> Berlin is-capital-of Germany, Paris is-capital-of France
zelph+> X has-capital Y
Answer: Germany has-capital yes
A statement that a rule derives asserts the facts its consequence writes in the same manner, yet it does not assert the value associated with a variable of the rule: the value the rule binds is claimed exclusively by the statement that asserted it (see Mentioning a Rule Is Not Asserting It).
Note: A line consisting of only a bare nested fact like
(subject rel object)is not a valid top-level statement in the REPL; nested facts are meant to be used as parts of a larger statement.Note: two values are separated by whitespace, so a value written directly against an opening parenthesis is not a statement:
a b x(c d e)is refused,a b x (c d e)is read. Function notationf(x)therefore exists only inside a notation island such as the standard library's$( ... )— see the math front end. The refusal names the token it stumbled over and the space that would fix it.
Braces: Set Constants and Collections¶
zelph distinguishes two kinds of unordered grouping, because mathematics and programming mean different things by "a set".
| Literal | What it is | Identity | Can membership grow? |
|---|---|---|---|
{a b c} |
set constant | its members | no |
@{a b c} |
collection | its own | yes |
A mathematical set is determined by its members — that is the axiom of
extensionality — so {a b} written twice denotes one and the same set, and
there is no operation that adds an element to it; you form a new set. A
collection is a container: it has an identity of its own, and putting
something into it is exactly what one does with it.
The @ marker follows Janet, the language zelph
embeds, where {...} is the immutable struct and @{...} the mutable table.
It costs no reserved character: @ stays an ordinary character inside names,
and only @{ is special.
Set constants¶
zelph> A in { elem1 elem2 elem3 }
A in {elem1 elem2 elem3}
Answer: elem2 in {elem1 elem2 elem3}
Answer: elem1 in {elem1 elem2 elem3}
Answer: elem3 in {elem1 elem2 elem3}
Two occurrences of the same literal are the same node, and the order inside the braces does not matter:
zelph> p q {a b}
zelph> r s {a b}
zelph> S q O
Answer: p q {a b}
zelph> S s O
Answer: r s {a b}
That identity is what makes a set literal usable in a rule: the set in the condition is the set in the data, so quantifying over its members works.
zelph> (X in {red green blue}) => (X is-a-colour yes)
(green is-a-colour yes) ⇐ (green in {red green blue})
(red is-a-colour yes) ⇐ (red in {red green blue})
(blue is-a-colour yes) ⇐ (blue in {red green blue})
zelph> S is-a-colour yes
Answer: green is-a-colour yes
Answer: blue is-a-colour yes
Answer: red is-a-colour yes
A statement that holds a variable quantifies over the members identically: (X p z) in {(x p z) c} inquires which member matches the structure X p z. Similar to the X in X in {red green blue}, the statement functions as a pattern over the set and never constitutes one of its members, thus the set remains {c (x p z)} as long as the question or a rule with this condition exists.
Membership in a constant is definitional rather than asserted, so extending one is refused — and the message names the literal that can be extended:
zelph> x in {a b}
Error in line "x in {a b}": fact(): a set constant cannot be extended -- {a b}
IS its members. Write the collection literal @{...} for a container that
membership can grow.
Saying what already holds is not an extension: a in {a b} is true by
construction and is simply a no-op.
For the same reason, two set constants never merge by name: .name declines to assign to {b c} the name currently held by {b d}, because equating the two would say that c equals d, rather than indicating that the set is {b c d}.
A rule consequence is the one place where the refusal arrives during
reasoning rather than while the line is read: X in {a b} is how a rule
quantifies over the members, so the pattern is fine and only its instances
are refused.
zelph> z rel {a b}
zelph> q p r
zelph> (X p Y) => (X in {a b})
! ⇐ (q p r)
└─ refused: a set constant cannot be extended -- {a b} IS its members. Write
the collection literal @{...} for a container that membership can grow.
Found one or more contradictions!
A refused consequence leaves the graph in its prior state. Whatever the firing built for it before the refusal goes with it – a statement that the consequence nests, a fresh witness, a member added to the rule’s own collection, the relation-type declaration (b ~ ->) for a node that the consequence used as a predicate for the first time – thus no other rule fires on it. The declaration stays only if something external to the firing still uses it: a fact utilizing that predicate, or a statement concerning the declaration. In the example below, the firing builds m q n for the outer statement before encountering the membership, and the rule over q derives nothing:
zelph> m p n
zelph> n ~ known
zelph> (X q Y) => (X ok Y)
zelph> (X p Y, Y ~ known) => ((X q Y) says (X in {a b}))
! ⇐ {(m p n) (n ~ known)}
└─ refused: a set constant cannot be extended -- {a b} IS its members. Write
the collection literal @{...} for a container that membership can grow.
Found one or more contradictions!
zelph> S q O
zelph> S ok O
A rule that a generator writes is refused similarly when it would extend a set constant, either within its consequence or within any of its conditions, and a typed statement that is refused retains none of the components it wrote before its own fact.
A consequence that the binding turns into a fact whose predicate is one of its objects is not written: typed, such a statement is refused, and a firing skips it without notification. It produces no residue either, neither a nested statement nor a fresh witness, thus (X p Y) => (Z Y X) adds no node to a p a, regardless of how frequently it runs.
Collections¶
A collection is built fresh by each literal, so two of them are two containers — and each takes its own members:
zelph> x in @{a b}
x in @{a b x}
zelph> y in @{a b}
y in @{a b y}
zelph> S in O
Answer: a in @{a b y}
Answer: x in @{a b x}
Answer: y in @{a b y}
Answer: b in @{a b y}
Answer: a in @{a b x}
Answer: b in @{a b x}
a and b appear twice because each literal built its own container and put
them into it. A set constant would have collapsed the two into one node.
This is what a rule can put things into. The container the rule writes is named by every fact it derives, and it grows as the run proceeds:
zelph> alice reported bug1
zelph> bob reported bug2
zelph> (X reported Y) => (Y in @{X})
(X reported Y) => (Y in @{Y X})
(bug1 in @{bug1}) ⇐ (alice reported bug1)
(bug2 in @{bug1 bug2}) ⇐ (bob reported bug2)
zelph> S in O
Answer: bug1 in @{bug1 bug2}
Answer: bug2 in @{bug1 bug2}
(Which of the two deductions appears initially depends on the execution – along with the member that the initial deduction reveals – and so does the sequence of the responses; refer to Querying.)
That container is not the collection written in the rule, though, but instead a node constructed by the rule for it: the collection’s term, a single node into which every firing of the rule writes. The rule names its own collection, the facts it derives name the term, and whatever the rule derives never ends up within the rule’s own collection. The rule thus continues to assert the same content, regardless of how much the term has gathered:
zelph> .list-rules
Listing all rules:
------------------------
(X reported Y) => (Y in @{Y X})
------------------------
(Y is a member because Y in @{X} puts it there.)
The term begins with the ground members of the rule’s collection, represented as data, and accumulates the outputs produced by the rule. A rule that writes to @{bug1 bug2} thus accumulates into a term that holds bug1 and bug2 from the start, and queries answer them like the members it gathered:
zelph> alice reported bug3
zelph> (X reported Y) => (Y in @{bug1 bug2})
(X reported Y) => (Y in @{bug1 bug2 Y})
(bug3 in @{bug3 bug1 bug2}) ⇐ (alice reported bug3)
zelph> S in O
Answer: bug3 in @{bug3 bug1 bug2}
Answer: bug1 in @{bug3 bug1 bug2}
Answer: bug2 in @{bug3 bug1 bug2}
A membership written within another statement, like (X p Y) => ((Y in @{X}) is noted), writes into the term in the same way, because a statement also asserts the facts nested in it, except when situated inside a rule (see Parentheses: Nested Facts).
Note the asymmetry, and that it is the point: quantify over a set constant to read its members, write into a collection to gather results.
One consequence is worth knowing before it surprises you: a printed collection
literal cannot be pasted back into a command. @{a b x} in a .explain or
.prune-facts pattern builds yet another container, so it never names the one
the answer came from — and the command says so:
zelph> x in @{a b}
x in @{a b x}
zelph> a in O
Answer: a in @{a b x}
zelph> .explain (a in @{a b x})
Fact is not asserted -- nothing to explain.
A collection literal @{...} builds a NEW container, so it cannot name an
existing one. Address the fact by its ID (.node without an argument reports the
last answer's node), or use a set constant {...}, whose identity IS its members.
zelph> .explain
a in @{a b x} [axiom]
The route that works is the argument-less form in the last line: it takes the node of the last answer, so no literal has to be re-parsed.
A set constant has no such restriction — its identity is its members, so
.explain (c in {c d}) addresses exactly the fact it names.
Which one a literal builds¶
Extensionality needs known members. A literal carrying a variable denotes a different set for every binding, so it cannot be hash-consed and is a collection:
zelph> a p b
zelph> (X p Y) => (X in {Y})
(X p Y) => (X in @{Y X})
(a in @{a}) ⇐ (a p b)
zelph> S in O
Answer: a in @{a}
The echo responds to the question the section asks: what was typed as {Y}
reappears as @{Y X}, a collection.
This is also what keeps the engine's own conjunction form working, whose
members are condition patterns and therefore never ground — see
the rule section for *{...} ~ conjunction.
A rule is regarded as a member with a variable wherever its variables occur, even within its conditions, thereby classifying both of these literals as containers, which when not inside a rule are displayed as the literal they were originally typed as. Neither serves as an answer to a query regarding its member, because a rule containing variables constitutes rule text, not data:
zelph> x has {((A r B) => (c q d))}
x has {((A r B) => (c q d))}
zelph> y has {((A r B, A s B) => (c q d))}
y has {(((A s B), (A r B)) => (c q d))}
zelph> S in O
A literal with a variable in a pattern¶
In a pattern – the condition within a rule, a query, or a statement held by a condition – such a literal represents a set: under a binding, it refers to the set formed by substituting its members, thus matching a set constant whose members correspond to its own in a one-to-one fashion. A set constant that varies by a single member, or holds one additional or one fewer member, constitutes a distinct set:
zelph> {(john enters card1) (card1 ~ card)} can nil
zelph> {(eve enters card3) (card3 ~ card) (card3 ~ red)} can nil
zelph> ({(X enters C) (C ~ card)} can nil) => (X ~ able)
(john ~ able) ⇐ ({(john enters card1) (card1 ~ card)} can nil)
zelph> {(X enters C) (C ~ card) (C ~ red)} can nil
Answer: {(eve enters card3) (card3 ~ card) (card3 ~ red)} can nil
When the members can form multiple pairings, such as {(X p Y) (X p Z)} versus {(a p b) (a p c)}, each possible pairing constitutes an independent match, and a guard or an additional condition determines which one is selected. Only a set constant is matched by its members, as it is its members: a collection may expand, so its current contents are not an enduring trait, and thus {(X p Y)} does not match @{(a p b)}. A literal lacking a variable is a set constant and is matched via identity, while a variable bound to a set refers to that node, just as it does for any other value. A literal that includes such a literal among its members is also a set pattern and is matched level by level: {{X} a} matches {{b} a}, and {a {b X}} matches {a {b q}}. Within a rule, a collection literal @{...} containing a variable is interpreted identically, since the rule prints its literals using that marker; in a query, @{...} builds a new container, which no fact names. Outside a rule, the literal appears exactly as originally entered, {...}, thus the echo of a query re-enters as the identical query.
A membership condition writes its subject into the literal it designates, just as a membership in a consequence does: (Z in {Y}) prints as (Z in @{Z Y}) and represents the set containing precisely Z and Y, thus matching the memberships of a set constant having two members.
A query functions as a statement too, and the variables inside it are what transform it into a pattern instead of raw data. Variables present in a set pattern are counted toward this, regardless of their placement among the members, unless they appear within a rule that a member mentions, in which case those variables are considered part of the rule. Upon answering the query, all content it wrote that holds one of its variables is removed – its statement, its set patterns and their memberships, the conjunction set derived from a comma list – while anything it wrote without a variable remains, just like a ground fact placed outside any condition operator is asserted. The collection that its literal built is an exception, regardless of whether it includes a variable: no fact outside the query names the newly created collection, so it is eliminated along with the facts that hold it. The same applies to the relation-type declaration established by the query’s first use of a predicate: X b Y, posed when b has never served as a predicate, declares b ~ -> just as any first fact of b would, and this declaration goes with the query unless a fact with that predicate that the query asserts, or a statement the query makes about the declaration, still uses it. The subsequent question thus answers with the fact alone, both in the current session and after a .save followed by a .load, and .explain as well as .node without an argument refer to the fact named by the last answer. .prune-nodes removes what such a variable binds, whether or not the variable is named: the sole variable in a pattern names what is erased wherever it resides, whether in a set or within a nested fact, except when it acts as the predicate of a fact, which the unnamed form rejects at any level, just as it does at the top level, and two such variables must be explicitly named:
zelph> {(john enters card1) (card1 ~ card)} can nil
zelph> {(bob enters card2) (card2 ~ ticket)} can work
zelph> {(X enters C) (C ~ card)} can nil
Answer: {(john enters card1) (card1 ~ card)} can nil
zelph> .explain
{(john enters card1) (card1 ~ card)} can nil [axiom]
zelph> S can nil
Answer: {(john enters card1) (card1 ~ card)} can nil
zelph> .prune-nodes {(X enters card2) (card2 ~ ticket)} can work
Pruned 5 matching facts and 1 nodes.
Consider running .cleanup.
zelph> X enters Y
Answer: john enters card1
zelph> .prune-nodes X ({(X enters C) (C ~ card)} can nil)
Pruned 5 matching facts and 1 nodes.
Consider running .cleanup.
zelph> S can nil
A literal a rule derives¶
A collection written in a rule belongs to the rule: it forms part of the rule’s text, just as the rule’s variables do, and no firing places it into the data. That holds exactly for a collection written with the rule – entered directly in its text, also present in a rule that another statement merely mentions, constructed within the argument forms of zelph/rule, or built by a generator for the rule it writes – and no subsequent writing alters it. A collection built as data – by a Janet program before rule construction, via zelph/rule*, whose arguments are built before execution, or through the C ABI – is a value that the rule refers to, and it receives what the rule writes.
Instead of a collection of the rule’s text, each firing writes one constructed specifically for that firing (the container a rule writes into is the exception, see below). While the rule is being read, the members of @{Y} remain unknown, yet a binding makes them known – thus, a literal in a consequence containing a member that is a variable, or a fact holding one, is reassembled using the substituted members, and becomes a set constant once the binding eliminates all variables within it. A collection among the members does not count: @{@{Y}} derives @{{b}}, a collection around a set constant. The rule continues to display its own collection; each derived fact carries its own set:
zelph> a p b
zelph> c p d
zelph> (X p Y) => (X likes @{Y})
(a likes {b}) ⇐ (a p b)
(c likes {d}) ⇐ (c p d)
zelph> S likes O
Answer: c likes {d}
Answer: a likes {b}
Set constants are hash-consed, meaning that re-deriving results in the same node. A literal that has nothing to substitute, like @{bucket}, or one whose members keep a variable following the binding, such as @{(Z q Y)} with its Z, stays a collection. The firing builds one for every binding of the consequence’s variables, holding the substituted members as data:
zelph> a p b
zelph> c p d
zelph> (X p Y) => (X likes @{bucket})
(a likes @{bucket}) ⇐ (a p b)
(c likes @{bucket}) ⇐ (c p d)
zelph> S in O
Answer: bucket in @{bucket}
Answer: bucket in @{bucket}
These are two collections that print alike, just as two typed @{a b} do. Each is identified by the collection in the rule and the binding it was built for, rather than by a counter, meaning that deriving the same fact again under the same binding targets the same node, and the run attains a fixpoint – unless what the rule builds flows back into its own binding. In that case, every pass generates a new collection, and the run never concludes:
(X p Y) => (X likes @{bucket})
(X likes C) => (C p k)
b p k
b p k builds a collection for b, the second rule makes that collection the subject of a p fact, which in turn generates a collection for it, and so on: b likes C1, C1 likes C2, C2 likes C3, …, four nodes per pass, and the same applies to @{(Z q Y)}. That is what the rules state – each collection is a new binding of X – and this is also the limit a rule that creates nodes via a fresh variable has. A set constant, or a named node, is the way to say that one shared object is intended: using {bucket} in the first rule, the example ends after three deductions. The same advice applies to memory: every collection produced by a firing is a node with a membership fact for each member, which the rules require and which no switch omits.
The container a rule writes into is not built per binding: Y in @{X} conveys information about that container, and each fact derived by the rule names its single term (refer to Collections). A collection literal in predicate position is built per binding just like any other: (X p Y) => (X @{b} Y) with a p c and d p e derives a @{b} c and d @{b} e across two relations that print alike. Insert a named relation, or the set constant {b}, when only one relation is meant.
A network saved by a prior version of zelph loads in the state it was stored, yet the collections within its rules are treated as data here, because that earlier zelph stored them just as it stored data: a firing names the rule’s own collection, the identical node for each binding. This is precisely how the older zelph handled a collection the rule writes to and a literal that has nothing to substitute. A literal grounded by a binding, like @{Y} above, it built for each binding as a set constant; here, the literal is included in the data with its variable. A rule re-typed here becomes a second rule beside the loaded one, since its literal now functions as a template. Saved by an older zelph after a p b and the rule, the example above proceeds as follows:
zelph> .load liked.bin
...
Note: rules in this network were saved by an older zelph, and their collections are data here; rebuild the network from its scripts before reasoning over it.
...
zelph-> c p d
zelph-> .run
...
(c likes @{Y}) ⇐ (c p d)
...
zelph+-> S likes O
Answer: c likes @{Y}
Answer: a likes {b}
Answer: a likes @{Y}
The older zelph derived c likes {d}, and a likes {b} is the fact it saved. In such a network, a generator writes its rules identically, using its own variable: following then go m, (G go H) => ((X p H) => (X likes @{H})) writes (X p m) => (X likes @{H}). Rebuild such a network from its scripts prior to conducting reasoning on it. The load prints a note requesting this action, except when a rule’s collection contains only what the rule’s own statement writes into it, as @{Y} does in (X reported Y) => (Y in @{Y}); in such cases, the rule derives as before.
Topology¶
Both kinds create a super-node representing the grouping itself, and link
the elements to it via the in (PartOf) relation.
- Facts created for
{A B}and for@{A B}alike: A in SuperNodeB in SuperNode
They vary solely in the method by which the super-node gets its identity: a set constant hashes its members, a collection is a fresh node – or, when a rule builds it, the node that the collection within the rule and the binding jointly determine. No other aspect of the graph distinguishes them, which is precisely why their printed representation makes the distinction: {...} versus @{...}.
The node nil is the empty literal of either kind – {} and @{}. No content exists for a super-node to collect, and nil is what an empty collection is across all of zelph. The sole exception is the data term of a collection in another rule’s text, which a rule binding that collection names before anything is derived into it (refer to what is substituted): while it is empty, it prints as @{}, and typed back, that is nil, not the term itself.
Angle Brackets: Lists¶
Angle brackets <...> create ordered lists implemented as classic Lisp-style cons lists using the core predicates cons and nil.
A list is represented by the outermost cons cell. There is no separate container node: the node returned by list construction is the list (exactly as in Lisp).
The empty list <> is nil, exactly as in Lisp: the terminator every
non-empty list ends at. q p <>, q p {} and q p nil are therefore the
same fact.
Two list syntaxes¶
zelph supports two input modes that both create cons lists:
- Node Lists (space-separated):
<item1 item2 item3> - Syntax: At least one whitespace between elements.
- Semantics: The elements are existing nodes (
item1,item2, …) and the list preserves this order. -
Construction: Built right-to-left:
Cell3 = item3 cons nilCell2 = item2 cons Cell3Cell1 = item1 cons Cell2← this outermost cons cell is the list
-
Compact Lists (continuous characters):
<abc> - Syntax: No spaces inside the brackets.
- Semantics: The input is split into individual characters. Each character is resolved to a named node (e.g.
"a","b","c"), and these become the list elements. - Construction detail: Before building the cons list, the character sequence is reversed.
So
"abc"becomes the element vector["c","b","a"], yielding:Cell3 = "a" cons nilCell2 = "b" cons Cell3Cell1 = "c" cons Cell2← this outermost cons cell is the list
This reversal is not "numeric logic" — it is simply the definition of the compact syntax and is useful for many right-to-left processing rules (including, but not limited to, arithmetic scripts).
Because of this rule, <abc> is internally identical to the explicit node list <c b a>.
Referring to the same cons list in different ways¶
The list node is the outermost cons cell. You can refer to the same list topology using any of these equivalent notations:
-
Explicit cons chain (nested facts)
(3 cons (1 cons nil)) -
Compact list syntax (character splitting + reversal rule)
<13> -
Node-list syntax (space-separated elements)
<3 1>
Example session:
zelph> (3 cons (1 cons nil)) is prime
<3 1> is prime
zelph> <13> is prime
<3 1> is prime
zelph> <3 1> is prime
<3 1> is prime
Why this works: <13> is parsed as a compact list of characters "1" and
"3", reversed before cons construction, so it becomes the same internal
cons chain as <3 1> — and the same node as the explicit cons chain.
To avoid confusion:
<13>is list syntax, not a numeric node ID. Numeric IDs are shown elsewhere in parentheses (e.g.(10)in Mermaid graphs).
Display: storage order, and the numeral exception¶
A cons list echoes in storage order, space-separated — the notation that
re-reads as the same list. <abc> therefore prints as <c b a>, which is
exactly the chain the compact syntax built; the display does not undo the
input-side reversal, because nothing about three character nodes says they
are a numeral.
The one exception is the numeral case, and it is opt-in: a script that
registers its digit alphabet with zelph/set-number-digits tells the
formatter which nodes are digits. A nil-terminated list consisting solely
of registered digits is then printed MSB-first as a decimal &-literal —
the inverse of the &-input syntax, not a spacing variant of <...>:
zelph> .import decimal-arithmetic
zelph+> <3 1> is prime
&13 is prime
zelph> <a b c> is x
<a b c> is x
See Number Literals for the mechanism. Either way the topology is untouched: display never imposes numeric meaning, it only reports the meaning a script has declared.
Digits vs. numbers (emergent, not built-in)¶
The cons-list representation naturally distinguishes between:
- a character node such as
"4"(a normal named node), and - a single-element list
( "4" cons nil )(a different node: a cons cell)
Whether you interpret "4" as a digit, or ( "4" cons nil ) as the number four, is entirely up to your rule system (e.g. stdlib/decimal-arithmetic.zph) and any external naming/mapping you choose to apply. For a detailed exploration of how rules can define arithmetic over these structures, see Semantic Math. For convenient input, the parser additionally supports &-prefixed number literals (e.g. &42), which delegate to the redefinable function zelph/number — so you can always type decimal even when the loaded arithmetic script uses a different internal base. See Number Literals. The inverse direction — displaying digit lists as decimal &-literals — works through the same opt-in mechanism: scripts register their digit alphabet via zelph/set-number-digits.
The Focus Operator *¶
A parenthesized statement evaluates to the fact it generates. Placing a * in front of any of its components causes it to evaluate to that component instead; the fact is still produced regardless.
(A B C)creates the fact and evaluates to the fact node.(*A B C)creates the fact and evaluates to nodeA.(*{...} ~ conjunction)creates the fact stating the set is a conjunction and evaluates to the set node.(A *=> B)evaluates to the rule, as(A => B)does: at the arrow, the focus names the rule itself.
A statement carries no more than one focus, and a statement nested within it is a distinct statement, with a focus of its own. A group enclosing a single value is that value itself, including its focus: x says (*(a p b)) is x says *(a p b) and evaluates to a p b, while (*k) says (*(l p m)) constitutes a statement with two foci, which is refused.
Example – a second fact on the side:
The focus enables a single expression to generate a fact and hand one of its components over to the surrounding statement:
zelph> (*tim ~ human) ~ male
tim ~ male
zelph> tim _predicate _object
Answer: tim ~ male
Answer: tim ~ human
The inner expression (*tim ~ human) creates the fact tim ~ human and — thanks to the * prefix — returns the node tim rather than the statement node. That returned node becomes the subject of the outer ~ male relation, so tim ~ male is created as well.
Querying tim _predicate _object (where leading underscores indicate variables, equivalent to using single uppercase letters) confirms that both facts are in the graph.
Example – the explicit form of a condition set:
The focus possesses a distinct role within rules. A comma-delimited sequence of conditions serves as a concise representation for a set of conditions marked with ~ conjunction, and the rule’s subject must be that set, not the tag fact – precisely what the focus gives:
zelph> (*{(X ~ human) (X ~ greek)} ~ conjunction) => (X ~ mortal)
((X ~ greek), (X ~ human)) => (X ~ mortal)
zelph> socrates ~ human
zelph> socrates ~ greek
(socrates ~ mortal) ⇐ {(socrates ~ greek) (socrates ~ human)}
The echo matches the comma list, as both formats generate the same rule: the comma list is the preferred form to write, while the explicit version reveals its fundamental meaning (see Rules and Inference). Absent the =>, the identical structure serves as the explicit notation for a query involving multiple conditions (refer to Querying). Within a condition, a focus alters what that condition is. Placed at the top of a condition in a comma list or a condition set, it makes that condition evaluate to a node, which inherently fails to match and thus is rejected. Positioned before the sole condition of a rule, it results in no rule at all: the statement reduces to a simple fact concerning the focused node, with => acting as the predicate. One level deeper, it creates a fact alongside, as demonstrated in the first example. This fact constitutes a claim just like any other and claims the facts nested in it: (x says *(a p b)) => (c q d) asserts x says (a p b) and, by extension, a p b, thereby firing the rule at once. The same applies when a rule has already written that fact as its pattern, and when the focus is positioned on a line by itself. Within a rule that a statement mentions, and within an argument of an opaque predicate, the fact that the focus writes is mentioned along with them, whereas a fact that was present beforehand keeps its status.
Example – the focus on a rule’s arrow:
On the arrow, the focus names the rule directly, which is what the statement of a rule evaluates to when no focus is applied. A line containing a single rule beneath a focus is interpreted identically, as a focus applied to an entire line lacks a surrounding statement to hand a part to. In both cases, the line is the rule statement it spells: when repeated, neither adds a second rule, and the ground parts of the rule stay patterns.
zelph> (X p Y) *=> (X q Y)
(X p Y) => (X q Y)
zelph> *((A p B) => (A q B))
(X p Y) => (X q Y)
zelph> .list-rules
Listing all rules:
------------------------
(X p Y) => (X q Y)
------------------------
The second line echoes the rule authored by the first, using that rule’s designated variable names. Within a statement, a focus on the arrow names the rule equally well: x notes ((a p b) *=> (c q d)) mentions the rule, just as x notes ((a p b) => (c q d)) does. A focus on any other part of a rule that a statement holds is refused: it would hand that part to the statement and leave the rule on its own, named by nothing, and therefore in force, even though a rule within another statement is merely mentioned. So is a focus on a single condition within such a rule’s comma list, which would change nothing. Any additional comma list – whether from a typed rule, a query, or one asserted by a statement – likewise refuses such a focus, because a comma list evaluates to the list as a whole, and ¬ does the same for a focus placed directly within it, as ¬ negates the whole pattern that comes after it. The arrow is the identical node whether enclosed in parentheses or within quotation marks, meaning that (a p b) (=>) (c q d) and (a p b) "=>" (c q d) both represent the rule (a p b) => (c q d), regardless of whether a focus is applied.
The Self-Fact Prefix :¶
Several standard-library modules use self-facts — facts whose subject and
object are the same node — as request markers: (N testprime N) triggers the
primality test, (T simplify T) a simplification. Because writing the term
twice is noisy (and error-prone for large terms), zelph provides a prefix
shorthand:
:pred X
is pure syntax sugar for
X pred X
The desugaring happens at parser level, so the shorthand works at any nesting
level: as a rule condition or consequence, inside conjunctions, and in the
repeatable query idiom. The operand is evaluated exactly once and used as both
subject and object, so both sides are guaranteed to be the same node — even
when the operand has side effects (a focus *, nested fact creation).
In the predicate position, it also carries a
path marker: :pred⁺ X is (X pred⁺ X)
and inquires whether X reaches itself, which is the manner in which a cycle
is detected.
The term self-fact is unique to zelph: in graph-theoretic terms, it represents a loop, and in relational terms, it corresponds to a point on the diagonal within the predicate – a conventional method for encoding unary predicates on a binary-relation substrate. For the viewpoint from logic, and for the rationale behind zelph’s decision not to assign operators a fixed arity instead, refer to Unary Predicates and Self-Facts.
zelph> .deductions off
Deduction printing mode: off
No derivations are printed; a run says how many it hid and marks the prompt
with '+' (e.g. "zelph+> ").
zelph> .import decimal-arithmetic
Note: 1 deduction was hidden.
zelph+> .import primes
zelph> :testprime &13
Note: 328 deductions were hidden.
zelph+> (:testprime &13) = X
Answer: (:testprime &13) = prime
zelph> (:isprime N, N hasdivisor D) => !
((N hasdivisor D), (:isprime N)) => !
Using .deductions off shortens this transcript: the primality test involves
a computation comprising 328 derivations. In default mode, 74 of these are
displayed while the remainder are concealed; with .deductions all, each
derivation appears individually, one per line (refer to
Deduction Output Modes).
A lone :pred on a line is an incomplete statement; the operand may follow on
the next line, so multi-line input works as usual. A variable token as
predicate keeps variable semantics: the rule pattern (:R X) matches any
self-fact, whatever its predicate.
Display. The formatter uses the same shorthand in the other direction:
any fact whose subject and object coincide is printed as :pred subject.
Because all terms are hash-consed, this includes facts that merely happen to
be self-facts, such as (&1 + &1) — printed :+ &1. A manual expansion of
such output back to the verbose S P S form parses to the identical node.
The sugar form is only used where it round-trips through the parser, and
that is a property of the predicate's NAME: it needs a single token free of
reserved characters (*, <, >, ,, quotes, ¬), and it has to print
without quotes, since the sugar has no place to put them. So a predicate
named &12 or ≈net — which the parser reads as a number literal and as a
neural condition — keeps the verbose form, as do (&9 * &9) and
x "is opposite of" x.
Modules can additionally exclude predicates from the display sugar via
zelph/no-selffact-sugar: the arithmetic and EML modules register their
term-forming operators, so a hash-consed coincidence like (&1 + &1) or
(&1 eml &1) prints verbose, while request markers (:testprime,
:simplify) keep the compact form. The registration only affects display;
:+ &1 remains valid input.
The colon is not a reserved character: atoms with an inner colon (URLs,
wd:Q5-style names) are unaffected; only a leading colon in value position
triggers the sugar. A node whose NAME begins with a colon is therefore
printed quoted (":foo" rel x), because bare it would open the sugar
instead — as a subject that is an arity error, and inside a multi-object
fact the sugar would swallow the next object without a word. The colon of
the sugar itself is not part of the name and is emitted beside it, which is
also what lets .run-export report the predicate as the node it is.
Creating a node graph¶
You can generate a node graph yourself using zelph's .mermaid command, which outputs a Mermaid HTML format file. For example:
.mermaid name 3
In this example, name refers to the node identifier (in the currently active language specified via the .lang command) whose connections you want to visualize. The number after it is the depth of connections to include. It defaults to 1, which does not mean a single hop: the depth then grows by itself until the graph holds enough nodes to be worth looking at. A second number caps how many neighbours each node contributes.
A node can also be named by the fact it is, written exactly as the fact prints, with or without parentheses:
.mermaid (a rel b) 3
With no argument at all the command takes the node from the last output, so a figure can be produced right after the statement that derived it, without naming anything twice.
To view the Mermaid graph, open the generated HTML file in a web browser.