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 |
Used to classify a condition in a rule as negative (match if the fact does not exist). |
| 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 when a relation contains spaces, it must be enclosed in quotation marks.
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.
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.
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
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.
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!
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}
zelph> y in @{a b}
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})
zelph> S in O
Answer: bug1 in @{bug1 bug2}
Answer: bug2 in @{bug1 bug2}
(The two deductions are printed as well; which of them comes first varies with the run, and so does the order of the answers β see Querying.)
The rule and the facts it derives name one and the same container, so what counts as its members depends on which of the two you are looking at. Inside the rule the variables are the statement; in the data the gathered members are. The rule therefore keeps saying what it says, however much it has gathered:
zelph> .list-rules
(X reported Y) => (Y in @{Y X})
(Y is a member because Y in @{X} puts it there.)
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 literal a rule derives¶
While the rule is being read its members are unknown, but a binding makes them known β so a container in a consequence is rebuilt from the substituted members, as a set constant. The rule keeps showing its own collection; each derived fact carries a set of its own:
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, which is what makes this safe: re-deriving lands on the same node, so the run reaches a fixpoint. A fresh collection per binding would be a new node on every run and never converge.
Two containers are deliberately not rebuilt. One is the container a rule
writes into: Y in @{X} says something about that container, so its
identity survives substitution and the accumulator above keeps naming one
bucket. The other is a literal with nothing to substitute β {red green} or
@{bucket} β which is already what it will be.
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 differ only in how the super-node gets its identity: a set constant hashes
its members, a collection is a fresh node. Nothing else in the graph
distinguishes them, which is why the printed form does: {...} versus @{...}.
The empty literal of either kind β {} and @{} β is the node nil. There is
nothing for a super-node to collect, and nil is what an empty collection is
throughout zelph.
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 *¶
When defining complex structures, you often need to refer to a specific part of an expression rather than the resulting fact node. The * operator allows you to "focus" or "dereference" a specific element to be returned.
(A B C)creates the fact and returns the relation node.(*A B C)creates the fact and returns nodeA.(*{...} ~ conjunction)creates the fact that the set is a conjunction, but returns the set node itself.
This operator is crucial for the rule syntax.
Example β typing a node via a nested fact:
The focus operator lets you create a fact and use its subject in an outer statement in a single expression:
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.
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 zelph's own: graph-theoretically a loop, relationally a point on the diagonal of the predicate β the classic way to encode unary predicates over a binary-relation substrate. For the logic-side perspective, and for why zelph does not assign operators a fixed arity instead, see Unary Predicates and Self-Facts.
zelph> .deductions off
Deduction printing mode: off
zelph> .import decimal-arithmetic
zelph> .import primes
zelph> :testprime &13
(skipped 328 deductions)
zelph> (:testprime &13) = X
Answer: (:testprime &13) = prime
zelph> (:isprime N, N hasdivisor D) => !
((:isprime N), (N hasdivisor D)) => !
.deductions off is what keeps this transcript short: the primality test is a
computation, and the 328 derivations it takes are printed line by line in the
default mode (see 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 visualise. The following number represents the depth of connections to include in the graph (default is 3).
To view the Mermaid graph, open the generated HTML file in a web browser.