Axioma Programming Language Manual
Version 0.9 Β Β· Β Calculemus! β Leibniz A multi-paradigm language for mathematics, logic, knowledge, and reasoning.
Calculemus! is Latin for "Let us calculate!" β the motto of the philosopher and mathematician Gottfried Wilhelm Leibniz (1646β1716), co-inventor of the calculus. Leibniz dreamed of a characteristica universalis: a formal language in which any idea could be written down exactly, paired with a calculus ratiocinator, a mechanical procedure for reasoning over it. Then, he wrote, two people who disagreed would no longer need to quarrel β they could take up their pens and say "Calculemus" ("let us calculate") and settle the matter by reckoning. Axioma is a step toward that vision: a language where logic, mathematics, and knowledge are things you compute with.
1. Introduction
Axioma is the language in which formal philosophy and symbolic logic become executable: a natural-language-readable surface over a first-class epistemic kernel β concepts, epistemic grounding, defeasible rules, and checkable proof. Where most languages compute plain values, Axioma computes values that carry their epistemic status: a fact's derivation provenance (its grounding), whether it is contradicted, and whether an answer is decidable at all. Its flagship demonstration is a runnable formalization of praxeology (the Rothbard corpus), carried end-to-end by that kernel with no special-case machinery.
Around that kernel, Axioma blends styles freely β set theory, first-order logic, multi-valued logics, lambda calculus, frame-based concepts, relational logic programming, stack-based programming, and natural-language definitions live side by side and compose with each other. The surface is deliberately consolidating, not accreting: redundant spellings are retired over time toward one canonical form per idea (see Β§3), so expressive breadth does not buy a second way to say the same thing.
Key features
- Concept system with natural-language syntax
(
concept Stock,Stock has price: 150). - Logic programming through deterministic, set-based pattern queries β Prolog-like relations without implicit depth-first backtracking.
- Six-grade epistemic grounding: every fact carries a
grade on the ladder
axiom > postulate > theorem > conjecture > hypothesis > datum, propagated through derivation as provenance. - Five first-class logics: Boolean, Kleene K3, Εukasiewicz L3, Belnap B4, GΓΆdel G3 (intuitionistic), automatically dispatched by operand type.
- Strict and defeasible rules in both backward and forward directions.
- Bilattice truth values with paraconsistent contamination (Belnap B4).
- SQLite-backed knowledge base shared with Cascade.
- Stack programming with both a user-accessible
Stacktype and a global interpreter stack. - MCP server exposing tools for AI-assistant integration.
- Tools:
axiomadoc(literate programming), VM mode, Wails GUI, web GUI, Jupyter kernel.
Influences
Axioma stands in a long line of languages and gratefully adapts the best of them, just as most languages do. For the record, the main lineages are:
- REBOL β the
:value binding, the family of scalar value literals (URL, email, file, money, pair, issue, β¦), the get-word (:w), refinements (name/ref), and the value-returning (non-throwing) error model. - Forth / Pop-11 β the stack model: the global interpreter stack, the postfix sequence notation, and the stack-shuffle verbs.
- SETL β set-theoretic data structures,
comprehensions, bags, and the
om/Ξ©undefined value. - Lisp / Scheme β the
'quote (the'wword literal), homoiconicity (code as data), and the S-expression view of the AST; Mathematica βfullformfor viewing the AST as a symbolic expression. - Prolog / Datalog β relational facts, rules, and queries.
- F-logic & Description Logic β the concept system: frame slots and the conceptβοΈrelationβοΈrule duality (F-logic), and subsumption / satisfiability reasoning over a tableau (Description Logic / ALC).
- Python, Haskell, Racket, Rust, Swift β assorted surface conveniences (comprehension spellings, string-escape and byte syntax, list accessors).
2. Getting Started
Installation
# Source checkout. If the repository is not publicly reachable yet, use the
# browser playground for zero-install evaluation until release archives are linked.
git clone https://github.com/vevenhar/axiomalang.git
cd axiomalang
go build -o axioma ./cmd/axioma/
./axioma --help
./axioma # Start REPL
Run a script
./axioma path/to/script.ax # Run a script
./axioma --no-kb script.ax # Skip Cascade KB preload (~10Γ faster startup)
./axioma --vm script.ax # Run in VM mode
./axioma --mcp # Start MCP server (stdio JSON-RPC)
Feature status at a glance
The browser column means the WebAssembly playground core: no host file system, no subprocesses, and no local SQLite KB. The native tree-walker is the reference runtime for the whole public language surface. The VM is useful for the compiled core, but some higher-level forms are intentionally still tree-walker-only.
| Feature surface | Browser playground | Native tree-walker | VM | Status |
|---|---|---|---|---|
| Core syntax, bindings, functions, arithmetic | yes | yes | yes | stable core |
| Collections, 1-based indexing, list comprehensions | yes | yes | yes | stable core; sets/dicts remain unordered |
| SQLite knowledge base, Cascade sharing, persistence | no | yes | use native tree-walker | native-only operational surface |
| File I/O and host/process integrations | no | yes | partial by feature | native-only where the host OS is required |
Foreign-language blocks (python, lisp,
sql, etc.) |
no | yes | yes for supported blocks | local runtimes/binaries required |
| Errors as values | yes | yes | partial | try / otherwise / attempt are
evaluator-only today |
| Macros, quasiquote-heavy code-as-data workflows | yes | yes | no | run under the tree-walker |
| ADT declarations and constructor-pattern matches | yes | yes | no | evaluator-only; --typecheck adds static
diagnostics |
First steps
axioma> a: {1, 2, 3}
{1, 2, 3}
axioma> b: {2, 3, 4}
{2, 3, 4}
axioma> a union b
{1, 2, 3, 4}
axioma> concept Person
axioma> Person has name: "Alice"
axioma> parent("John", "Mary")
axioma> {Y | Y <- parent("John", Y)}
{"Mary"}
3. Language Fundamentals
Bindings β one canonical form
Axioma has a single canonical value-binding
operator: :. It's the shortest binding form of any
mainstream language (two characters of overhead), puts the name first,
and composes naturally with type annotations.
x: 5 # bare binding
radius: 3.14 * 2 # expression on the RHS
a, b, c: 1, 2, 3 # multi-assignment (parallel)
d :: Day: Mon # with type annotation
All variants compile to the same LetStatement AST node.
A multi-assignment evaluates every right-hand side first and then binds
(so a, b: b, a swaps), updates each name wherever it
already exists within the enclosing function frame (declaring it here
otherwise β exactly the single-name rule), and evaluates to the
tuple of all assigned values, so the REPL echoes
(1, 2, 3). A multi-assignment may also open a
bracket block β
if val > best then [ best, at: val, idx ] β the leading
name, name run is recognized as a statement, not array
elements, so branch bodies can start with a parallel update (both the
: and = spellings; plain [a, b]
stays an array). The axioma/beginner subset uses only the
bare form; multi-assign and type annotations show up later in the
curriculum.
=is a find-or-update synonym of:.=declares a name if it is absent and updates it if present β for any name and case β so textbook mathematics reads verbatim:B = {2, 4, 6},C = A union B,Pi = 3.14159.:stays the canonical binding (and the form to learn first);=is the math/textbook spelling. (=is no longer strict-update-only.)Casing is context-local, not a global type tag. Both
:and=admit a name of any case βB: {2, 4, 6}andB = {2, 4, 6}are set variables. The right-hand side decides concept-vs-value: aconcept { ... }RHS names a Concept (uppercase required βStock: concept { ... }; a lowercase LHS with aconceptRHS errors), while any other value binds an ordinary variable. Uppercase still means a logic variable inside rules (the Prolog convention) and is the Concept-naming convention β it just no longer forces a top-level binding to be a Concept.
AandEare ordinary identifiers, not quantifier shorthands. Quantify withforall/exists, the glyphsβ/β, the backtick digraphs`forall/`exists, orβ!(unique existential).E!(the free-logic existence predicate) is a separate token.
There is no
letor:=. Bind with:(canonical) or=. Writinglet x = valueorx := valueis a syntax error with an inline hint pointing atx: value.
Three ways to introduce a name
: is the canonical value binding; two further
keywords introduce a name lazily. They differ in
when the right-hand side runs and what they can
name:
| Form | RHS evaluation | Use for |
|---|---|---|
x: expr (or x = expr) |
eager β runs at the binding | ordinary values |
declare x = expr |
lazy β runs on first use, then memoized (computed once, then cached, so later reads don't re-run it) | deferred / expensive values; /persist Β·
/transient control session persistence |
define β¦ |
lazy + memoized (value form), or a definition | concepts, types/schemas, and knowledge claims |
x: 1 + 2 # eager β computed now
declare big = slow_query() # lazy β runs on first read of `big`, then cached
define concept Point = { x: 0, y: 0 } # a definition, not a value
: evaluates now; declare and
define defer until the value is first needed and then reuse
the cached result. Beyond that, declare adds the
persistence refinements, while define is the gateway to the
definition / speech-act family β define concept /
define axiom / define postulate /
define theorem / define word /
define dialect (see Β§13).
Scoping & shadowing
Axioma is block-structured: every block form pushes
a nested lexical environment β function bodies,
if/then/else branches,
comprehensions, foreach loops. A fresh
name bound inside a block dies with the block (stricter than Python,
where if-branch and loop variables leak out):
if true then [z: 42]
z # ERROR: undefined β the branch scope is gone
doubled: [n * 2 | n <- [1, 2, 3]]
n # ERROR: undefined β comprehension-local
foreach w in [10, 20] [t: w]
w # ERROR: undefined β loop-local
A binder binds; a body assignment stops at its frame. That is the whole rule, and the two halves are worth stating separately.
Body assignment β name: value (and its
= synonym) β is find-or-update, but the search
stops at the enclosing function frame: it walks the
blocks around it, and if the name exists anywhere within that frame it
is updated in place; otherwise it declares here. So one
function can never disturb another function's binding by accident:
v: 1
h: func() [v: 2] # declares h's OWN v; the outer one is untouched
h()
v # β 1
x: "outer"
f: func(x) [x + "-inner"] # a PARAMETER is always a fresh binding
f("param") # β "param-inner"
x # β "outer" β untouched
The boundary is the frame, not the block, and that
distinction is doing all the work. Every block inside one function β an
if branch, a loop body, a value block β is transparent to
the search, which is what keeps accumulators and counters working:
sum_all: func(xs) [
total: 0
foreach n in xs [total: total + n] # writes the frame's own total
total
]
sum_all([1, 2, 3, 4]) # β 10
countdown: func(n) [
i: 0
while i < n [i: i + 1] # a block-local i would never terminate
i
]
Binder positions, by contrast, always introduce a fresh
binding that shadows any outer name of the same
spelling. A binder is any position where a form names the variable it is
about to bind for you: a parameter, a loop variable, a comprehension
generator, a quantifier variable, a relation logic variable, a
match capture.
i: "outer"
foreach i in [1, 2, 3] [ ] # the loop variable is the loop's own
i # β "outer" β untouched
sq: func(xs) [{i * i | i <- xs}]
sq([1, 2, 3]) # β {1, 4, 9}
i # β "outer" β a comprehension is an expression,
# and expressions have no side effects on you
rebind β
writing a name in an enclosing frame
Because a body assignment stops at its own frame, reaching
out of a function is spelled, not assumed.
rebind walks the whole chain, updates the nearest binding
it finds, and takes : or = like any other
binding:
counter: 0
bump: func() [rebind counter: counter + 1]
bump()
bump()
counter # β 2
rebind never declares. A name that is bound nowhere is
an error, which is the point: rebind
states that the name already exists, so a typo in it is caught rather
than quietly becoming a second variable.
bump2: func() [rebind countr: 1]
bump2() # ERROR: 'countr' is not bound in any enclosing scope
Two details worth knowing. rebind targets the
nearest enclosing binding, not the outermost β so a function
that has its own d and calls an inner function that rebinds
d updates that middle one, not a top-level d.
And none counts as bound, so x: none then
rebind x: 5 works; declaring a slot empty and filling it
later stays spellable.
axioma --lint finds the writes that no longer
reach. A body that meant to update an outer name now
declares a local instead, and nothing errors:
acc: 0
bump: func() [acc: acc + 1] # reads the outer 0, writes a NEW local
bump()
bump()
acc # β 0
axioma --lint script.ax
Nothing is run: the file is parsed and each function body is read for
one shape β a plain assignment whose target name is bound outside the
function and is neither a parameter nor a binder variable. A path may be
a file or a directory; # lint: ok on the offending line, or
the line above it, says the local was deliberate. Exit status is
non-zero when anything is reported, so it works as a gate.
The trade: closures mutate captured state, but say so. A closure still reaches the cell its factory declared β it just spells the reach:
make_counter: func() [
acc: 0
func() [
rebind acc: acc + 1 # reaches the CAPTURED acc
acc
]
]
c: make_counter()
c() # β 1
c() # β 2
c() # β 3
Each call to make_counter() still gets its own
acc; two counters do not share a cell. (acc
here is just a name; count also works β it stopped being a
reserved word in July 2026 and is now a soft keyword, recognized only in
the constrained-language shape
count <<pattern>>.)
Builtins vs constants. The lowercase math names
(pi, e, tau, β¦) are
shadowable fallback builtins β pi: 3 wins locally
and leaves the system untouched. The canonical UPPERCASE constants
(PI, TAU, EULER, β¦) are seeded
immutable: PI: 3 reports
Cannot reassign constant 'PI' (non-fatal) and
PI keeps its value.
Regression pins:
tests/axioma/binding/test_scoping_shadowing.ax.
Guarded identifiers and atoms
To use a reserved word, a multi-word name, or
punctuation as an identifier, guard it with $:
$forall: 5 # `$name` β a reserved word used as a plain name
$"interest rate": 0.0525 # `$"..."` β spaces / punctuation in a name
$"GDP growth %": 2.1
$ disambiguates by what follows it: a
digit keeps the money literal ($5,
$5.00); a " opens a quoted guard; a letter
opens a bare guard. The ${...} interpolation form inside
strings is untouched β guards never use it.
Symbolic set elements (atoms) use the self-denoting
word literal 'name, and cardinality is
len(...):
A = {'p, 'q, 'r} # a set of three atoms
'q in A # β true
len(A) # β 3 (the cardinality of A)
Persistence refinements
The : operator deliberately has no refinement
slot β adding /persist to : would
force three-token lookahead on every identifier parse, and
: is one of the highest-frequency tokens in the language.
For persistent value bindings, use the dedicated declare
form:
declare/persist counter = 0 # Saved to .axioma_session.bin
declare/transient temp = 42 # Discarded at session end
declare scratch = 0 # No refinement β uses the mode default
Default: REPL persists, scripts are transient.
Note the operator: declare uses = (the
ASSIGN token), not :. This keeps the canonical
: form refinement-free and concentrates the persistence
vocabulary in one place. The same /persist /
/transient refinements apply to axiom,
postulate, and define:
axiom/persist gravity_constant = 9.81
postulate/transient working_hypothesis = "..."
Persistence-controlled bindings are written
declare/persist counter = 0(anddeclare/transient).
The public contract is the syntax above plus the persistence notes in Β§15.
Statement vs. expression syntax
Most language constructs come in dual forms:
| Operation | Statement form | Expression form |
|---|---|---|
| Property assignment | usa.gdp: 27000 |
usa[gdp -> 28000] (returns the value) |
| Concept definition | concept Stock |
Stock: concept {} (concept-literal RHS) |
| Fact assertion | assert parent("a", "b") (relation declared) |
insert("parent", "a", "b") |
| Retraction | retract [...] |
forget("parent", "a", "b") |
Note that s: a Stock {} is not a
concept-definition form β the indefinite article creates an
instance (a ConcreteEntity) of an already-defined
concept, a different operation entirely (see Β§13).
Use the natural-language statement form for concept lifecycle and properties, the functional/expression form for values and computation.
Typing the glyphs
Axioma's math notation (β βͺ β© β β β β Ξ» β§ β¨ β β β€ β₯ β» β¦)
is always optional β the operators have plain word
twins (in, union, intersect,
subset, superset,
subsetneq/supsetneq for the proper forms
β β, forall, exists,
lambda, and, or,
implies, xor for β»,
!=), so no setup is ever required. The exceptions are the
description-logic pair β β and the NAND/NOR pair
βΌ β½, which stay glyph/digraph-only as infixes (the words
nand/nor are their prefix builtins,
deliberately unreserved). When you do want the glyphs, pick
whichever input layer fits where you're typing β all of them resolve the
same names from the same catalog (the one symbols()
prints), so a spelling learned once works everywhere:
| Where you're typing | How | Example |
|---|---|---|
| Any source file, any editor | backtick digraph β pure ASCII that lexes as the glyph | C `sqcap D β‘ C β D (no word form exists);
`forall β‘ β |
| Canonicalize a whole file | axioma --glyphify file.ax (inverse:
--asciify) |
x in U and P β x β U β§ P |
| The REPL (and wizards) | type \in then Tab |
\forall β₯ β β |
| VS Code / Cursor / Neovim / Helix | \in + accept the completion (served by
axioma-lsp) |
\cup β βͺ |
| Browser playground | \in then Tab |
\subseteq β₯ β β |
| Anywhere on your system | the generated espanso pack
(resources/espanso/axioma-symbols.yml) |
:in: β β |
| macOS, no installs | System Settings β Keyboard β Text Replacements; or Unicode Hex Input
(β₯2208 β β) |
Names are LaTeX first (`cup, \subseteq),
then Axioma's canonical names (`union), then word aliases β
~100 spellings. Discover them from inside the language:
symbols() lists the whole catalog with LaTeX names and
meanings; glyph("cup") looks one up.
The MVL truth-value literals (Β§9) live in the same catalog β
symbols("mvl") lists all 13 (β€β₯α΅,
?α΅, Β½Ε, ?β±, β¦), each with
digraphs from the same table: `belboth β‘ β€β₯α΅,
`klunknown β‘ ?α΅, `lhalf β‘
Β½Ε, plus Priest's `glut / `gap.
The REPL/LSP/playground/espanso layers above pick them up automatically
(\belboth + Tab β β€β₯α΅).
Notes. The digraph leader is a backtick, not the
Agda/Julia backslash, because \ is Axioma's set-difference
operator; a backtick outside strings and comments was previously a
syntax error, so the form is collision-free. A digraph is byte-identical
to its glyph at the token level, so it works in every construct, in the
VM, and in the playground. --glyphify is token-aware
(strings and comments are never touched) and verifies the rewrite lexes
and parses identically before writing; conversions only happen where
both spellings produce the same token, so
not/Β¬ and value literals like
true/false/om are deliberately
left alone.
4. Data Types
Primitives
| Type | Examples | Notes |
|---|---|---|
Integer |
42, -17, 0,
0xFF, 0b1010_1100, 0o755,
1_000_000, 2^100 |
Arbitrary precision β integers never overflow (see below); hex/binary/octal prefixes; underscore separators |
Float |
3.14159, -2.5, 0.75,
1.5e6, 3.14e-2, 1E+9,
0x1p-1, 0xA.Bp2 |
IEEE 754 double; scientific notation e/E Β±
optional sign; hexadecimal floats (Go/C99 syntax β
binary exponent p/P required:
0x1p-1 = 2β»ΒΉ = 0.5, 0xa.bp2 =
42.75, 0x2.p3 = 16.0). The
exponent is what distinguishes a hex float from a hex integer
(0x10 stays Integer 16), keeps
0xFE/0x1E reading
e/E as digits, and keeps 0x15e-2
a subtraction. Lua's exponentless fraction 0x0.2 is
deliberately rejected with a hint (write 0x0.2p0, or
evaluate the Lua form via [lua/eval | 0x0.2 ]) |
Rational |
1/3, rational(2, 6) β
1/3 |
Exact p/q on big integers, GCD-reduced β /
on integers stays exact (1/3 + 1/6 β 1/2,
never 0.4999β¦); accessors numerator(r) /
denominator(r) |
Complex |
complex(3, 4) β 3 + 4i |
The top of the numeric tower β every other numeric
type embeds, so complex(3, 4) + 1/2 β
3.5 + 4i. Full arithmetic incl. ^ (exact at
integer exponents: complex(0, 1)^2 β -1) and
unary minus;
sqrt/exp/log/sin/cos/abs/conjugate
all accept one. No ordering. Embedding is via float64, so
exactness stops here |
String |
"hello", "unicode: ββ",
"\u{2203}", r"raw \n" |
UTF-8; escape sequences + r"..." raw prefix β see Strings |
Boolean |
true, false |
Classical two-valued |
Byte |
byte(0xFF) |
Single byte 0..255; distinct from Integer. See Binary data |
Bytes |
b"hello", b"\xff\x00",
bytes(0x48, 0x69) |
Immutable byte sequence |
Integers don't overflow
Integer is arbitrary-precision (the
Python/Mathematica model, not C/Lua's fixed 64-bit wrap-around): when a
result outgrows the machine word it promotes transparently, so
9223372036854775807 + 1 is
9223372036854775808, 2^100 is exact, and
3^39 has every digit right. This holds identically in the
evaluator and under --vm (the VM delegates its integer core
to the reference runtime, so the semantics agree by construction), and
across the numeric builtins (succ/pred,
sum, abs,
divmod/quotient/remainder,
floor/ceil/round/trunc/int,
gcd/lcm, factorial, β¦).
Consequences of the design:
- There are no
maxinteger/minintegerconstants β a largest integer does not exist. (Python 3 removedsys.maxintfor the same reason; Julia has notypemax(BigInt); Haskell'sIntegerhas noBoundedinstance.) - Hex/binary/octal literals are magnitudes, not bit
patterns:
0xFFFFFFFFFFFFFFFFis 2βΆβ΄β1 (a positive number), not Lua's-1. - Bit operators
(
band/bor/bxor/bshl/bshr) use Python's infinite-two's-complement reading:bshlis exact at any count (1 bshl 63= 2βΆΒ³,1 bshl 100= 2ΒΉβ°β°), andbshris an arithmetic shift that drains to0(or-1for negatives) past the width. - Memory is the only bound. An operation whose
single result would be astronomically large (a shift count or
exponent past the ~67-million-bit budget) raises a catchable error
rather than exhausting memory:
try(1 bshl 10^12)is anErrorvalue, not a crash. - The representation is invisible:
type(2^100)is"Integer"like any other integer. Mixing with aFloatconverts to float64 (precision may be lost past 2β΅Β³ β the universal int-meets-float rule).
Dual null types
Axioma has two distinct null-like values with different semantics β they are not interchangeable.
| Value | Semantics | Truthiness | Display | Use for |
|---|---|---|---|---|
none, null |
Absent / nil / missing | Falsy | null |
Uninitialized data, missing fields |
om, Ξ© |
SETL "omega" β a value that exists but is undetermined | Falsy | Ξ© |
Database unknowns, undefined computations |
none == om # false β never interchangeable
if none then ... # never executes
if om then ... # never executes (om is falsy)
Operational rule: none/null means absence;
om/Ξ© means an explicit undetermined value.
They compare differently and have different truthiness. Because
om is falsy β it folds onto the
designation law, so designated(om) is false and a bare
if om then β¦ runs the else branch β
if/while/filter read
om as absence. To branch on undeterminedness as a VALUE,
test explicitly with == om (or is om). This
makes om agree with the typed
Kleene unknown ?α΅, which it equals and which is likewise
falsy by designation (if ?α΅ also takes the else-branch; see
Β§9).
Multi-valued logic types
Written as literals (the same forms the interpreter prints) or built via constructors; both participate automatically in operator dispatch (priority Belnap > Intuit3 > Εukasiewicz > Kleene > Boolean). See Β§9.
glut: β€β₯α΅ # Belnap glut literal (β‘ belnap("both"))
half: Β½Ε # Εukasiewicz half (β‘ lukasiewicz(0.5))
unknown_g3: ?β± # GΓΆdel G3 unknown (β‘ intuit3("unknown"))
score: lukasiewicz(0.73) # constructor β any value in [0, 1]
(contradiction itself is a reserved word β hence
glut, which is also Priest's term for the value.)
Strings β escape sequences, raw form, codepoint builtins
String literals are written between double quotes
("β¦") or single quotes ('β¦'). They are stored
as UTF-8, so Unicode characters can appear directly in source:
greeting: "hello, world"
math: "βx βy. x + y = 0"
greek: "Ξ» Ο Ξ©"
For cases where the character can't be typed directly, escape sequences decode at parse time:
| Escape | Result |
|---|---|
\n \t \r |
LF, TAB, CR |
\\ \" \' \0 |
literal \, ", ', NUL |
\u{H...H} |
Unicode codepoint, 1β6 hex digits, full U+0000βU+10FFFF |
"line1\nline2" # 2 lines (LF in the middle)
"tab\there" # 3 fields separated by TAB
"quote: \"hi\"" # quote: "hi"
"\u{2203}" # β β (BMP β 4 hex digits)
"\u{1D54A}" # β π (math S β 5 hex digits, supplementary plane)
"\u{1F600}" # β π (emoji β 5 hex digits)
"\u{10FFFF}" # β τΏΏ (max valid Unicode codepoint)
The braced \u{H...H} form takes 1β6 hex digits. The
4-hex "\uXXXX" form is intentionally not
supported β it only reaches the Basic Multilingual Plane and forces a
second \U00000000 escape for everything above U+FFFF. The
single braced form handles the entire codepoint range.
Surrogate codepoints rejected. \u{D800}
through \u{DFFF} are rejected at parse time because they
cannot appear in valid UTF-8. chr(N) enforces the same
check at runtime.
Unknown escapes are preserved verbatim.
"regex \d+" keeps \d as a literal
\ followed by d β a back-compat hatch for
strings that contain regex metacharacters. Define a proper escape only
when needed; everything else passes through.
Raw strings β r"..." and
r'...'
The r prefix bypasses escape decoding entirely. Useful
for paths, regex patterns, and any genuinely-literal content:
winpath: r"C:\Users\alice\new" # literal path β no \U, \n decoding
pattern: r"\d+\.\d+" # literal regex
raw_unicode: r"\u{2203}" # β 8 chars: \, u, {, 2, 2, 0, 3, }
The result is byte-identical to the source between the quotes.
r"β¦" and "β¦" produce the same
*ast.StringLiteral AST node β the raw form just skips the
decode pass.
Triple-quoted and interpolated strings
Both also decode escapes:
multi: """
line one
line two \u{2203} ${some_var}
"""
x: 42
interp: "value = ${x}, glyph = \u{2200}"
Codepoint builtins β
chr and ord
For programmatic codepoint construction (when the literal form can't help because the value comes from runtime):
chr(8707) # β "β"
chr(0x1F600) # β "π"
ord("A") # β 65
ord("β") # β 8707
ord(chr(N)) == N # round-trip for any valid codepoint
Both accept the full Unicode range U+0000..U+10FFFF; surrogates are
rejected; ord("") errors with "empty string has no
codepoint".
| Form | In Axioma |
|---|---|
"\n" "\t" etc. |
β decoded |
"\u{H...H}" (1β6 hex, braced) |
β decoded |
"\uXXXX" (4-hex, no braces) |
β use "\u{XXXX}" |
"\xHH" (byte hex) |
β use b"\xHH" for bytes |
"\N{NAME}" (Unicode name) |
β (future, optional) |
r"..." / r'...' |
β raw |
chr(N) / ord(s) |
β |
Unicode normalization β
normalize(s, [form])
The same visible text can arrive in different codepoint spellings:
Γ© is either the single codepoint U+00E9 or e
followed by the combining acute U+0301. The two render identically but
are invisible to == and count
β one is 1 codepoint, the other 2. normalize rewrites a
string to a canonical form so comparison and counting behave:
"e\u{301}" == "\u{E9}" # false β the problem is real
normalize("e\u{301}") == normalize("\u{E9}") # true β NFC composes both
count("e\u{301}") # 2
count(normalize("e\u{301}")) # 1
normalize("\u{E9}", "NFD") # decomposes β "e\u{301}"
normalize("\u{FB01}", "NFKC") # "fi" β compatibility fold (ο¬)
normalize("\u{2460}", "NFKC") # "1" β β folds too
try(normalize("x", "NFX")) # catchable Error naming the forms
Forms: NFC (default β the W3C/web recommendation and
JavaScript's String.normalize default),
NFD (decomposed),
NFKC/NFKD (additionally fold
compatibility characters β ligatures, circled digits, full-width forms).
The form name is case-insensitive. Normalize both sides
before comparing text from mixed sources (file systems, user input,
copy-paste β macOS file names, for instance, arrive decomposed).
Grapheme clusters remain out of scope: count counts
codepoints, so a combining sequence with no precomposed form still
counts per codepoint.
Substring search β
index_of and span_of
Two builtins locate a substring, both in 1-based character
(rune) coordinates β the same coordinates
substring, s[a..b] slicing, and
nth use, so a found position feeds extraction directly:
index_of("hello Lua users", "Lua") # 7 β 1-based START position
index_of("hello", "zz") # none β a miss is the falsy `none`
# (composes with if/??; never 0)
s, e: span_of("hello Lua users", "Lua") # (7, 9) β the (start, end) PAIR,
substring("hello Lua users", s, e) # "Lua" 1-based INCLUSIVE
span_of("abc", "z") ?? "absent" # miss β none (falsy; composes)
index_of("aXbXc", "X", 3) # 4 β optional init: search FROM there
span_of("aXbXc", "X", -2) # (4, 4) β negative init counts from
# the end; result is ABSOLUTE
index_of(collection, target [, init]) answers where
does it start β on strings, arrays, and tuples (a Set
errors: no positions β use in).
span_of(s, sub [, init]) answers where does it
live: a Tuple of inclusive (start, end)
positions, or none when absent. The optional
init starts the search at that position (below 1 clamps to
1; an empty needle matches at the clamped init, up to one
past the end), and because returned positions are absolute, the
scan-all-occurrences loop is direct:
p: index_of(haystack, needle)
while p > 0 [
# ... use p ...
p: index_of(haystack, needle, p + 1)
]
Rune coordinates mean multibyte text is safe by construction:
span_of("hΓ€llo wΓ€lt", "wΓ€lt") β (7, 10), and
substring with those positions returns exactly
"wΓ€lt". For pattern-based (not literal) search, use the
regex family below; for a membership test alone,
contains(s, sub) / sub in s.
Regular expressions
β native regex_* builtins
Five native builtins wrap Go's RE2 engine (linear-time, no
catastrophic backtracking). Argument order is
(subject, pattern) throughout. An invalid
pattern returns a catchable Error value rather than
crashing the host.
| Builtin | Returns | Example β result |
|---|---|---|
regex_match(s, pat) |
Boolean |
regex_match("a1b2", "[0-9]") β true |
regex_find_all(s, pat) |
Array<String> |
regex_find_all("a1b2c3", "[0-9]") β
["1","2","3"] |
regex_replace(s, pat, repl) |
String |
regex_replace("John Smith", "(\\w+) (\\w+)", "$2 $1") β
"Smith John" |
regex_split(s, pat) |
Array<String> |
regex_split("one two", "\\s+") β
["one","two"] |
regex_captures(s, pat) |
Array<String> |
regex_captures("2026-06-14", "(\\d+)-(\\d+)-(\\d+)") β
["2026-06-14","2026","06","14"] |
regex_replace supports $1/$2
backreferences; regex_captures returns group 0 (the whole
match) followed by each capture group, or [] on no match.
Use a raw pattern (r"\d+\.\d+") to avoid double-escaping. A
typical tokenize-then-convert pipeline:
nums: regex_find_all("temp 38.5 hr 72 sat 0.97", "[0-9.]+") # ["38.5", "72", "0.97"]
float(nums[1]) > 38.0 # β true
Binary data β Byte and
Bytes
Distinct from Integer and String so the
type system can dispatch byte-specific operations and so
bs[0] == 0xff reads as a
Byte/Byte comparison rather than implicit
coercion. The cost is verbosity, the win is no silent UTF-8
corruption.
# Three construction forms
b1: byte(0xFF) # single byte, 0..255 β errors otherwise
b2: bytes(0x48, 0x69) # variadic β each arg 0..255 or Byte
b3: bytes([72, 105]) # from Integer array
b4: bytes("Hi") # from String (UTF-8 byte view)
b5: b"Hi" # b"..." literal
b6: b"\xff\x00\x01" # hex escapes (also \n \r \t \\ \" \0)
| Form | Result |
|---|---|
byte(0xFF) |
Byte(255) β explicit narrowing |
bytes(...) |
Bytes from variadic, array, or String |
b"..." |
Literal β escape-decoded at parse time |
int(b) |
Widen Byte β Integer |
Operations
# Length, indexing (1-based), slicing, concat, equality
len(b"hello") # 5
b"hello"[1] # Byte(104) β that's 'h'
b"hello"[2:4] # b"ell"
b"AB" + b"CD" # b"ABCD"
b"AB" == bytes(0x41, 0x42) # true
Conversions (explicit + fallible)
| Function | Returns | Errors when |
|---|---|---|
bytes_to_array(bs) |
Array of Byte (β‘ bs[i]
elements; bytes(arr) round-trips) |
never |
bytes_to_hex(bs) |
"ff00ab" |
never |
hex_to_bytes(s) |
Bytes |
input has odd length or non-hex chars |
bytes_to_string(bs, "utf-8") |
String |
bytes aren't valid UTF-8 |
string_to_bytes(s, "utf-8") |
Bytes |
encoding unknown |
base64_encode(bs) / base64_decode(s) |
round-trip | decoder errors on bad input |
read_bytes(path) /
write_bytes(path, bs) |
file I/O | path missing / permission |
Bitwise ops β word-form infix (v3) + functional form
Symbolic bitwise operators (& |
^ << >>) all conflict
with existing Axioma syntax (& is address-of,
| is comprehension separator, ^ is
POWER). Word-form operators sidestep the conflict and match
Axioma's pattern of and / or /
not / union / intersect.
# Word-form infix (precedence: SUM β same as +/-, binds tighter than ==)
0xF0 band 0x0F # 0
0xF0 bor 0x0F # 255
0xFF bxor 0x0F # 240
1 bshl 4 # 16
0x80 bshr 4 # 8
0xFF band 0x0F == 0x0F # true β (0xFF band 0x0F) == 0x0F
# On two Bytes: stays Byte for &|^, also for shifts
byte(0xF0) band byte(0x0F) # Byte(0x00)
byte(1) bshl byte(4) # Byte(0x10)
# Mixed Byte/Integer: widens to Integer
byte(1) bshl 4 # Integer(16)
# Functional form (still available β useful when you need a callable)
bit_and(byte(0xF0), byte(0x0F)) # Byte(0x00)
bit_or(byte(0xF0), byte(0x0F)) # Byte(0xFF)
bit_xor(byte(0xFF), byte(0x0F)) # Byte(0xF0)
bit_not(byte(0x0F)) # Byte(0xF0)
bit_shl(byte(1), 4) # Byte(0x10)
bit_shr(byte(0x80), 4) # Byte(0x08)
reduce(bit_or, byte(0), bytes_to_array(bs)) # fold OR over bytes β Byte
The functional forms are ordinary builtins, and builtins are valid
higher-order callables β map / filter /
reduce / df_filter accept them directly
(map(bit_not, xs), reduce(bit_or, 0, xs)), the
same way the Enumerable verbs (sort_by,
detect, β¦) always have. The one exception is
partial, which needs declared parameters to curry and so
takes user functions only.
Shift counts must be 0..63. Shift by a larger amount errors rather than wrapping.
Arithmetic on Byte widens to Integer (no
overflow surprise β explicit byte((a+b) % 256) to
wrap):
byte(200) + byte(100) # Integer(300) β wider than 255
byte((int(byte(200)) + 100) % 256) # Byte(0x2c) = 44 β wrap explicitly
Integer literal prefixes
Adding bytes also brought standard hex / binary / octal literals to the language:
0xFF # 255
0xFF_AB # 65451 β underscores for readability
0b1010_1100 # 172
0o755 # 493
1_000_000 # 1000000 β underscores work in decimal too
These produce Integer, not Byte β wrap with
byte(0xFF) for the byte form.
AST inspection
fullform(b"AB") # "Bytes(65, 66)"
headof(b"AB") # "Bytes"
argsof(b"AB") # ["65", "66"]
fullform(byte(0xFF)) # "byte(255)" β AST of the call, not the value
Design notes
Bytedisplays as decimal.byte(0xFF)prints as255β the Python / Go / Rust convention for byte values. For a hex rendering usebytes_to_hex(bytes(b)). (Byteskeeps theb"..."literal form with\xffescapes.)- Encoding-aware separation.
Bytesdoesn't carry encoding metadata; conversion toStringis explicit and fails on invalid input. - Immutable. Operations return new
Bytesrather than mutating in place β composes cleanly with comprehensions, rule derivation, and the VM's bytecode constant pool. - 1-based indexing matches
Array/String/Tuple. - A
Stringindexes by CHARACTER (rune), aBytesby byte."ÀÀb"[2]β"Γ€"(the 2nd character; negative indices count characters from the end,"rΓ©sumΓ©"[-1]β"Γ©"), consistent withnth/substring/ the slice forms / the ordinal accessors β sos[i] == nth(s, i)always, under both runtimes. Byte-level access on a String is explicit:string_to_bytes(s)[i]β the i-th byte. (Before July 2026,s[i]selected the i-th byte and could return mojibake on multi-byte characters; the VM rejected string indexing entirely.) - Colon-slice
x[lo:hi]is 1-based and INCLUSIVE acrossBytes/Array/String/Tupleβ identical tox[lo..hi], so"hello"[2:4]β"ell"(3 elements). This is not Python's 0-based half-open slice ("hello"[1:3]β"el", 2 elements); pasting a Python slice yields a different, silently-valid result. Use..(inclusive) or..<(half-open) when you want to be unambiguous about the bound style. - Slice edge policy (all spellings β
lo:hi,lo..hi,lo..<hi): out-of-range bounds clamp ("hello"[5:10]β"o",xs[2..9999]β the tail) and a reversed window yields the empty value ("hello"[4:2]β"",a[2..1]β[]β so the recursion idiomarr[2..len(arr)]is safe on a 1-element array with no guard). A slice never raises an out-of-bounds error. The two-bound../..<forms lower to the same slice node as:at parse time, so all three run identically under--vm; an end-lessxs[2..]slices to the end (β‘xs[2:]β arrays, strings, tuples). Descending selection spells its step explicitly βxs[5..1..-1]β reversed (the steppedx[lo..hi..step]form keeps the directional range path, evaluator-only); a step-lessx[4..2]is an empty window, not a reversal. - No infix bitwise ops in v1 β adding them would touch lexer + parser + VM. Functional form unblocks bit work now; infix sugar can come later.
- Bit pattern matching (Erlang's
<<v:4, len:16, payload/binary>>β the most expressive byte primitive of any language) is provided in v2 aspack/unpackbuiltins using Python'sstruct-style format strings (see next subsection).
See tests/axioma/bytes/test_bytes.ax for a 48-assertion
smoke test that runs identically under both the evaluator and
--vm.
Binary serialization β
pack / unpack
Python-struct-style format strings. pack
serializes values into Bytes; unpack reads
Bytes back into a Tuple of typed values. The
format spec is small enough to memorize:
bs: pack(">BBH", 1, 2, 256) # b"\x01\x02\x01\x00" (4 bytes)
header: unpack(">BBH", bs)
println(header[1], header[2], header[3]) # 1 2 256
# TCP-header-style parse: port (u16), length (u16), flags (u16), seq (u32)
packet: b"\x04\xd2\x00\x10\x00\x20\x00\x00\x00\x01"
parts: unpack(">HHHI", packet)
println(parts[1], parts[2], parts[3], parts[4]) # 1234 16 32 1
Format string grammar:
Endian prefix (optional):
<little,>big,!network (= big),="native" (= big). Default is big-endian.Type codes (each optionally preceded by a count):
Code Size Pack from Unpack to Notes b/B1 Integer / Byte Integer / Byte signed / unsigned 8-bit h/H2 Integer Integer signed / unsigned 16-bit i/I4 Integer Integer signed / unsigned 32-bit q/Q8 Integer Integer signed / unsigned 64-bit f4 Float Float IEEE 754 single d8 Float Float IEEE 754 double sN String / Bytes Bytes counted byte field, pads/truncates c1 Bytes(1) Bytes(1) single-byte field x1 (none) (none) pad byte β consumes 0 values Count prefix:
4B= four unsigned bytes (consumes 4 args). Fors, the count is the byte-length of the field:4spacks/unpacks a 4-byte string field.
Out-of-range values error rather than silently wrap:
pack(">B", 300) # ERROR: pack 'B': value 300 out of range 0..255
pack(">b", 200) # ERROR: pack 'b': value 200 out of range -128..127
Round-trip identity:
unpack(">d", pack(">d", 3.141592653589793))[1] # 3.141592653589793
unpack(">q", pack(">q", -9_000_000_000))[1] # -9000000000
See tests/axioma/bytes/test_pack_unpack.ax for the full
48-assertion smoke test.
Slicing (v2)
Bytes slicing now works under --vm (the
OpSlice opcode added in v2 also unlocked slicing for
String, Array, and Tuple in the
bytecode engine):
b"Hello World"[1:5] # b"Hello"
b"hello"[3:] # b"llo" β open end
b"hello"[:3] # b"hel" β open start
Endian-aware read/write at offset (v3)
Offset-style protocol parsing sugar over
pack/unpack. Each builtin reads or writes a
single field of fixed width at a given 1-based offset
(matching Axioma's indexing convention). Reads return values; writes
return a new Bytes (originals are
immutable).
| Builtin | Returns | Notes |
|---|---|---|
read_u16_be(bs, off) /
read_u16_le(bs, off) |
Integer 0..65535 | unsigned 16-bit |
read_i16_be / read_i16_le |
Integer Β±32767 | signed 16-bit (sign-extended) |
read_u32_be / read_u32_le |
Integer 0..2^32-1 | unsigned 32-bit |
read_i32_be / read_i32_le |
Integer Β±2^31 | signed 32-bit |
read_u64_be / read_u64_le |
Integer | may wrap to negative for > int64 max |
read_i64_be / read_i64_le |
Integer | signed 64-bit |
read_f32_be / read_f32_le |
Float | IEEE 754 single |
read_f64_be / read_f64_le |
Float | IEEE 754 double |
write_* (same suffix family) |
Bytes | new copy with field overwritten |
# Parse a 10-byte packet (u16 port, u16 length, u16 flags, u32 seq)
packet: b"\x04\xd2\x00\x10\x00\x20\x00\x00\x00\x01"
port: read_u16_be(packet, 1) # 1234
length: read_u16_be(packet, 3) # 16
flags: read_u16_be(packet, 5) # 32
seq: read_u32_be(packet, 7) # 1
# Build a response β start with zeros, overwrite fields
resp: bytes(0, 0, 0, 0, 0, 0, 0, 0)
resp1: write_u16_be(resp, 1, port)
resp2: write_u16_be(resp1, 3, length)
# resp is still b"\x00\x00\x00\x00\x00\x00\x00\x00" β original untouched.
These compose with pack/unpack: round-trip
identity holds. Use them when you want to read individual fields at
known offsets without computing slice ranges, or when you want to mutate
a buffer in protocol-style.
See tests/axioma/bytes/test_bitwise_endian.ax for a
39-assertion v3 smoke test that runs identically under both engines.
Scalar value types & literals
Axioma has a family of lexer-recognized scalar
literals β you write a URL, an e-mail address, a file path, a
date, money, or a 2-D pair directly, and the lexer gives it a
first-class type. type() returns a TitleCase string that
agrees with the @ sigil and the primitive-type concepts
(@v, type(v), and v is T all
match).
| Type | Literal | type() |
Predicate | Notes |
|---|---|---|---|---|
URL |
https://example.com/path |
"URL" |
is_url |
http:// / https://; read()
fetches it |
Email |
[email protected] |
"Email" |
β | local@domain shape |
File |
%data/file.txt |
"File" |
β | % + path; relative (see the gotcha
below) |
Date |
2026-05-02, 1-Jan-2024,
7/2/26 |
"Date" |
β | several spellings |
Time |
12:34:56 |
"Time" |
β | HH:MM:SS |
Money |
$123.45 |
"Money" |
is_money |
$ + digit |
Pair |
100x200 |
"Pair" |
is_pair |
2-D integer pair XxY |
Issue |
#123 |
"Issue" |
β | # + digit |
Percent |
42% |
"Percent" |
is_percent |
number + % |
Word |
'hello |
"Word" |
is_word |
self-evaluating symbol |
GetWord |
:name |
"GetWord" |
β | reads a value without evaluating |
QuoteWord |
#hello |
"QuoteWord" |
β | # + letter |
type(https://example.com) # β "URL"
type($123.45) # β "Money"
type(100x200) # β "Pair"
type(42%) # β "Percent"
is_url(https://example.com) # β true
is_pair(100x200) # β true
is_url($5) # β false
Sigil disambiguation β position and the next character
decide. $ + digit is Money
($5), otherwise $name / $"β¦" is a
guarded identifier. % + alphanumeric is a File
(%data) β but only in prefix position: a
% glued to the tail of a preceding value is the modulo
operator (7%2 β 1), and a number takes a
trailing % as a Percent literal only when no
letter or digit follows (50% is a percent;
100%7 is 100 mod 7). # + digit is
an Issue (#123), # + letter is a
QuoteWord (#hello).
Arithmetic on
Pair / Money / Percent
100x200 + 20x30 # β 120x230 (component-wise)
100x200 * 2 # β 200x400 (scalar scale)
$100 + $25 # β $125
$100 * 1.5 # β $150
10% * 200 # β 20 (percent of a number)
10% * $200 # β $20 (percent of money)
10% + 5% # β 15% (percent Β± percent stays a Percent)
$200 + 10% # β $220 (base + p% β increase by p percent of itself)
$200 - 10% # β $180 ; 100 + 10% β 110 (plain numbers too)
Percent Β± percent stays in percent space
(10% + 5% == 15% β true; comparisons return
Booleans). base Β± p% adjusts the base by p percent
of itself β the percent goes on the right
($200 + 10%, not 10% + $200), the money result
is exact and displays to cents (so $19.99 + 7.25%
shows $21.44 though its value is exactly
$21.439275 β see "Money is exact" below), and left-assoc
chaining compounds sequentially ($200 + 10% + 5% β
$231). Compound assignment inherits:
price += 10%. Multiplication is still the only
other operator that crosses Percent with numbers or Money β
50% * 25%, 10% / 2, and mixed equality
(10% == 0.1) remain errors.
Money is exact
Money carries an exact rational value β
$1/3 is exactly one third of a dollar. Arithmetic never
rounds; rounding happens at exactly two points β
display and the explicit
round(money, places) β and nowhere
else.
($1 / 3) * 3 # β $1 EXACTLY β the third comes back
$1234567.07 + $0.01 # β $1234567.08 (no float drift)
($0.01 / 3) * 3 # β $0.01 a penny split three ways survives
$5 == $5.00 # β true ; len({$5, $5.00}) β 1 ; $5 in {$5.00} β true
$100 * 0.1 # β $10 β a Float scalar is read by its DECIMAL spelling,
# so no binary-0.1 drift (use a Rational for a
# value a decimal cannot spell)
Display rounds to the cent (half-to-even), so a non-terminating amount looks like an ordinary price β but the stored value is exact, and comparison uses it:
"" + ($1 / 3) # β "$0.33" β DISPLAYS roundedβ¦
($1 / 3) == $0.33 # β false β β¦but a third of a dollar is not 33 cents
round($1 / 3, 2) # β $0.33 β round() materializes cents as an exact value
round($1 / 3, 2) == $0.33 # β true
round($1 / 3, 2) * 3 # β $0.99 β rounded, then tripled, loses the third
So $1/3 and $0.33 print alike yet are
different amounts (and key differently in a set) β the same "display is
a rounded view" that Float already has. When you genuinely
want cents, round(m, 2) is the one operation that rounds
the value. (Cross-currency arithmetic still errors; display is
two decimals for every currency.)
DateTime &
Duration β the datetime package
The lexer's Date / Time literals above are
date-only / time-of-day scalars. Full timestamps and elapsed-time values
are the DateTime and Duration types, built by
the datetime package β ambient since July
2026 (datetime.now() works with no import; see
Β§27.2), with import β¦ as remaining the renaming
spelling:
import "builtin:datetime" as Dt
bday: Dt.date(1990, 5, 15) # midnight timestamp
type(bday) # β "DateTime"
Dt.year(bday) # β 1990 (month / day / hour / minute / β¦ too)
Dt.format(bday, "2006-01-02") # β "1990-05-15" (Go layout strings)
gap: Dt.time_between(Dt.date(1990, 6, 15), bday)
gap # β 744h0m0s (later argument FIRST β earlier-first is negative)
type(gap) # β "Duration"
Dt.format(Dt.add(bday, gap), "2006-01-02") # β "1990-06-15" (add a Duration to a DateTime)
type(Dt.now()) # β "DateTime" (Dt.utc() for UTC)
is_datetime(bday) # β true ; duration?(gap) β true
The package also carries
Dt.datetime(y, mo, d, h, mi, s), Dt.from_unix
/ Dt.unix, Dt.from_string /
Dt.strftime, calendar accessors (weekday /
yearday / week), and
Dt.add_date(t, years, months, days).
Words β 'w, :w,
#w
A word is a value (a symbol), distinct from a variable you look up.
'hello # β 'hello (natural word β self-evaluating)
x: 42
:x # β 42 (get-word β reads the bound value, no call)
#tag # β #tag (quote-word β a literal symbol)
:xis the get-word;@xis not.@is the type-of sigil, so@xreturns"Integer"(the type ofx), while:xreturns42(the value). Use:xto read a value without evaluating it as a call.
read and file I/O
read(source) is the file/URL read verb β one argument,
returns a String:
source |
Example | Behavior |
|---|---|---|
String path |
read("/tmp/notes.txt") |
read the file |
String / URL http(s) |
read(https://example.com) |
HTTP GET β body |
%file literal |
read(%data/notes.txt) |
read the (relative) file |
f: "/tmp/notes.txt"
write(f, "Hello from Axioma!") # β true
file_exists(f) # β true
read(f) # β "Hello from Axioma!"
append(f, " More.") # β true
remove(f) # β true (delete the file)
file_exists(f) # β false
write / append return a
Boolean; non-string content is stringified. A failed
read (missing file, network error) returns a catchable
Error value, not a crash. For a richer path/line API,
import "builtin:io" as IO exposes
IO.read_file, IO.read_lines,
IO.join_path, etc.
Naming gotchas (reserved-word collisions).
- Existence is
file_exists(path), notexists(...)β the bare wordexistslexes as the existential quantifier (β), soexists("/p")is a parse error.file_existsmatches the io package'sIO.file_exists.- Deletion is
remove(path), notdelete(...)βdeleteis the reserved retract keyword.Tag(<tag>) is shadowed by the natural-language<β¦>literal, sotype(<html>)is"NaturalLanguage", not"Tag"β treat<tag>as unavailable from source.- Absolute
%/abs/pathdoes not lex (the character after%must be alphanumeric); use theStringformread("/abs/path")for absolute paths.
Runnable tour:
tests/axioma/rebol/rebol_data_types_showcase.ax. Assertion
tests: tests/axioma/rebol/test_rebol_datatypes_and_read.ax
and tests/axioma/io/test_io_functions.ax. Full reference:
resources/docs/Data Types.md.
Ranges β ordered
a..b, exclusive ..<, by step,
open n..
a..b is a first-class ordered
Range value β it knows its direction, its step, and
(optionally) that it has no end. It displays compactly, tests membership
in O(1), and iterates in source order everywhere:
r: 1..5 # an ordered Range value (displays as 1..5)
type(r) # β "Range"
5..1 # descending β walks 5, 4, 3, 2, 1
1..<5 # exclusive upper bound β 1, 2, 3, 4
1..10 by 3 # stepped β 1, 4, 7, 10 (`by` takes a positive
10..1 by 3 # MAGNITUDE; direction comes from the operands:
# 10, 7, 4, 1)
1..10..3 # SIGNED-step spelling β 1, 4, 7, 10 here, but NOT a
10..1..-3 # mere alias of `by`: the sign gives the direction
# (β 10, 7, 4, 1), and a sign that contradicts the
# bounds yields an EMPTY range (10..1..3 β {}) where
# `by` refuses outright (1..10 by -3 β error).
# `range(a, b, step)` follows the SIGNED rule.
# Both spellings read bare in a loop header:
# `for i in 10..1..-3 [β¦]`, `repeat i <- 1..10..3 [β¦]`.
"a".."e" # character ranges too (single-character bounds)
2.. # OPEN-ENDED β lazy and infinite: 2, 3, 4, β¦
50 in 1..100 # β true β O(1), never materializes
3 in 1..10 by 2 # β true; 4 in 1..10 by 2 β false (the step grid counts)
(1..5).length # β 5 β the sequence accessors work (Β§5)
array(5..1) # β [5, 4, 3, 2, 1] β materialize in SOURCE order
Ordered in, ordered out. Every consumer walks a
range in its own order: foreach/for loops,
comprehensions ([x * x | x <- 5..1] β
[25, 16, 9, 4, 1]), and the collection builtins β
map/filter over a range return an ordered
Array, reduce/foldr fold in
source order, and
nth/secondβ¦tenth/sample/sort/min/max/sum/product/zip/
enumerate all take ranges directly. (Before July 2026 a
range materialized as an unordered Set, which is how
zip(1..len(a), a) could silently scramble β the motivating
bug for the Range type.)
Set operations decay to Set. A range used where a
set is meant becomes the set of its points:
(1..5) union {9}, (1..9) intersect (4..12),
set(1..3), powerset(1..3), and the
{1..5, 99} set-literal splice all produce ordinary
Sets.
Open-ended ranges are lazy. 2.. never
materializes; bounded consumers work and unbounded ones refuse
loudly:
first(2.., 4) # β [2, 3, 4, 5] (pull as many as you ask for)
zip(1.., ["a", "b"]) # β [(1, "a"), (2, "b")] β truncates to the finite side
9 in 2.. # β true (arithmetic, still O(1))
first((x * x | x <- 1..), 5) # β [1, 4, 9, 16, 25] β lazy comprehensions stream
foreach k in 1.. [ β¦ if done then [break] ] # loop until YOU stop it
sum(1..) # catchable Error β as are len, array, map, sort,
# set-op decay, `.last`, and every other materializer:
# an open range never hangs the interpreter
Open ranges are exact past machine precision β
first(2^100.., 2) returns [2^100, 2^100 + 1]
exactly. A finite range with bounds beyond int64 errors
loudly instead of clamping (spell it open if you need the big
start).
Two spelling notes. The end of a range must sit on the same
line as the .. β a .. that ends the
line reads as an open range, so a line-broken 1.. +
5 is two statements. And in index position
a missing end means "to the end": a[2..] slices from
position 2 (β‘ a[2:]), following the slice clamping rules in
the Bytes design notes above.
The literal replaces most uses of the older builders, which remain:
range(5) β the Range 1..5
(1-based, inclusive) and setrange(1, 5) β a Set.
range(a, b[, step]) is the call spelling of the same value
the .. operator builds, so range(1, 5) == 1..5
β it returned an eager Array until July 2026, which made it the one
constructor whose result type disagreed with its name. Write
array(range(β¦)) where you want the materialized list.
Runtimes agree β the VM compiles ranges to the same Range
value (byte-identical output, pinned by
tests/axioma/cli/test_ordered_range_vm.sh).
A Range reads like an Array wherever reading makes sense:
len, sum, sort,
min/max,
map/filter/reduce,
first/last/nth, in,
zip, indexing r[2] (negative counts from the
end), slicing r[2:4], reverse,
contains, count, index_of,
unique, rest, and [h | t]
destructuring. Every one of them answers exactly what it answers for
array(r). Writing is not in the surface β push
on a Range is an error, by design. An open range
answers where arithmetic can ((1..)[4],
contains(1.., 400)) and otherwise raises a catchable
"cannot materialize an open-ended range" rather than walking
forever.
Tests: tests/axioma/types/test_ordered_range.ax,
test_range_step_by.ax,
test_range_open_ended.ax,
tests/axioma/builtins/test_range_builtin_consumers.ax.
5. Collections & Stacks
Arrays
xs: [1, 2, 3]
xs[1] # 1
len(xs) # 3
A trailing comma is allowed, and it is the way to
write a one-element array unambiguously:
[x,]. This matters because [...] is also a
block body (in if cond then [body] else [body] the branch
brackets are blocks, so a bare single-statement [x]
evaluates to x). The comma forces the array
reading in every position:
[5,] # β [5] (a 1-element array)
["w1",] # β ["w1"] (len 1 β NOT the string "w1")
[1, 2, 3,] # β [1, 2, 3] (trailing comma in n-arrays too)
if c then ["a", "b"] else ["w1",] # else-branch is a 1-element array
if c then [42] else [0] # bare [x] β the scalar 42 / 0 (block body)
Since the PiL ch5 parity round (July 2026) the trailing comma is
valid in all three literal forms β {1, 2,}
(set) and {a: 1, b: 2,} (dict) parse like
[1, 2,] β so generated constructors never special-case the
last element. It also holds inside a function body,
where [x,] used to be a SyntaxError.
What a bracket means, by position
A body is a statement sequence; a
branch and a value binding are
expression positions. They agree except on a bare comma run, which
reduces as Pop-11 postfix in a body (2, 3, + β
5, the third notation for the same identity) and builds an
array in the other two:
| written | body func f() [β¦] |
branch then [β¦] |
value v = [β¦] |
|---|---|---|---|
[e] |
e |
e |
[e] array |
[stmt β stmt] |
last value | last value | last value |
[a, b] |
Pop-11 sequence | [a, b] array |
[a, b] array |
[e,] |
[e] array |
[e] array |
[e] array |
[] |
none |
[] |
[] |
The difference is deliberate and load-bearing in both directions:
making the branch a body slot costs the [x,] idiom and
every array-returning branch, while making a body's commas build arrays
removes Pop-11 postfix from function bodies. Write [e,]
when you want the array in any position, and parenthesize a tuple
([(a, b)]) when you want one in a body.
Positional access on sets, list surgery, and range loops (PiL ch5 round)
Four additions from the Programming in Lua chapter-5
comparison (July 2026), each with full --vm parity:
s: {30, 10, 20}
s[1] # β 10 β the index operator now answers on sets,
s[-1] # over the SAME canonical sorted order that
s[1..2] # nth/first/`.second` always used (s[i] β‘
# nth(s, i) by construction); slices come back
# source-shaped ({10, 20}); s[1] = x stays a
# loud error β sets have no positions to WRITE
insert_at([10, 20, 30], 1, 15) # β [15, 10, 20, 30] β Lua's
remove_at([10, 20, 30], 1) # β [20, 30] table.insert/remove,
remove_at([10, 20, 30], -1) # β [10, 20] FUNCTIONAL (a new
# array; the input is untouched, the
# push contract; read the element first
# with xs[pos] β pop returns elements)
foreach i in 1..len(xs) [ β¦ ] # bare ranges after `in` β the Lua/Python
foreach i in 2..n [ β¦ ] # numeric-for idiom, no parens needed
foreach i in 1..xs.length [ β¦ ] # property access works in bare bounds
foreach v in d.xs [ β¦ ] # β¦ and as the iterable itself; chains
# (d.inner.ys, len(d.xs)) compose too
The range spelling also fixed a latent ordering bug: ranges (and set
iteration generally) used to walk in randomized Go-map
order β foreach i in (2..n) could visit 3, 4, 2 on
one run and 2, 3, 4 on the next. Ranges now iterate
numerically and sets iterate in their canonical
sorted order, so every loop is deterministic run to run. (Since
July 2026 that guarantee is structural: a..b is a
first-class ordered Range value with
direction, step, and open-ended forms β see Ranges in
Β§4.)
Classic pitfall
β seeding an accumulator with 0
The most common bug in a hand-rolled maximum loop is invisible on the data you first test with:
maximum: func(a) [
best: 0 # β the bug: a hidden claim that
at: 0 # every element is β₯ 0
for i, e in enumerate(a) [
if e > best then [
best = e
at = i
]
]
return (best, at)
]
maximum([8, 10, 23, 12, 5]) # (23, 3) β looks perfect
maximum([-4, -2, -9]) # (0, 0) β silently WRONG: no element
# beats the seed, and 0 isn't even
# a valid position
The seed of an accumulator loop must be either the
operation's identity element or a value from the data
itself. 0 is the identity of + (and
1 of *) β the identity of max is
negative infinity, which is why the 0 seed
smuggles in an assumption. Three correct forms, most idiomatic
first:
max(a) # the builtin β max([-4, -2, -9]) β -2;
(max(a), index_of(a, max(a))) # pair it with index_of for the position
best, at: a[1], 1 # seed FROM THE DATA β works for any
# numbers; empty input now errors loudly
# (guard with a.empty) instead of
# returning a plausible wrong answer
best: -inf # or seed with max's true identity β
# every number beats -inf
The transferable habit: a constant seed encodes a claim about
the data β test the claim, not just the happy path. Two inputs
kill this whole bug class in review: a list of negatives and the empty
list. (Note max([]) returns null β the
builtins follow the query-never-crashes convention; a hand-rolled
version must choose its own empty-input answer.) Axioma can also make
the claim executable: declare
ensures: result[1] in a on the function and every call β
and check maximum over random inputs β polices it. See Contracts β
requires / ensures /
check f.
Sequence
accessors β xs.length, xs's first,
xs.indexed
Array, Tuple, String, Set, and Range carry a small closed set
of read-only accessors, available in both the dot and the
possessive spelling. Each mirrors its builtin twin
(length/size β‘ len,
first/last β‘
first()/last(), indexed β‘
enumerate(xs)):
| Accessor | Returns | Notes |
|---|---|---|
length / size |
Integer | β‘ len(xs) (String: byte length) |
first / last |
edge element | null when empty β a query never crashes |
second β¦ tenth |
k-th element | ordinals β‘ second(xs)β¦tenth(xs);
null when out of range |
empty |
Boolean | the guard for first/last |
indexed / enumerated |
Array of (i, e) pairs |
1-based, index-first β the indexed view for loops and comprehensions |
nums: [10, 20, 30]
nums.length # β 3 ; nums's length β 3 (same accessor)
nums.first # β 10 ; nums.last β 30
nums.second # β 20 ; nums's third β 30 (ordinals second..tenth)
[].first # β null (empty: no crash β check xs.empty first)
[99].second # β null (out of range β null, where second([99]) errors)
"hello".last # β "o" (String edges β and ordinals β are whole runes)
{3, 1, 2}.first # β 1 (sorted order, same as first(set))
{3, 1, 2}.second # β 2 (set.second == second(set))
(1, 2, 3).size # β 3 ; {}.empty β true
nums.indexed # β [(1, 10), (2, 20), (3, 30)] β the pairs feed the
# indexed loop forms (Β§6) and comprehension destructure
"ab".indexed # β [(1, "a"), (2, "b")] (runes, like the edges)
{30, 10, 20}.indexed # β [(1, 10), (2, 20), (3, 30)] (canonical sorted order)
(3..1).indexed # β [(1, 3), (2, 2), (3, 1)] β ranges keep DIRECTION
(1..).indexed # catchable Error β an open range can't materialize;
# loop it with `for e at i in 1..` + break instead
The accessors are read-only
(nums.length: 5 is an error) and the accessor table is
deliberately closed β but an unknown name on one of these receivers now
falls back to the unary vocabulary (next section), so
xs.sum answers sum(xs). Multi-argument
transformation stays function-first (map(f, xs), not
xs.map(f) β the fallback is strictly unary). Hashes
are not included: a hash's dot namespace is its own keys
(d.length reads the user key length), so hash
metadata stays with len(d) / keys(d) /
values(d). count is not an accessor name (it
is the reserved comprehension keyword), and nth has no
accessor form (it takes an argument β nth(xs, k) stays a
builtin). The dot spelling has full --vm parity; the
possessive is evaluator-only, like all possessive forms.
Lists and recursion β
[h | t]
Arrays are the sequence. [h | t] conses
onto one and destructures one, so the classic recursive list algorithms
are written directly on arrays β there is no separate list type to
convert to and from.
func total([]) [0]
func total([h | t]) [h + total(t)]
total([1, 2, 3, 4]) # β 10
The same spelling works in four positions:
# 1 β value position: cons
[1 | [2, 3]] # β [1, 2, 3]
[1 | []] # β [1]
# 2 β multi-clause function head (above)
# 3 β match arm
tot: func(xs) [
match xs with
| [] => 0
| [h | t] => h + tot(t)
]
# 4 β relation head, where it UNIFIES in both directions
relation append(a, b, c)
append([], Ys, Ys) :- true
append([X | Xs], Ys, [X | Zs]) :- append(Xs, Ys, Zs)
{Z | Z <- append([1,2], [3], Z)} # forward β {[1, 2, 3]}
{(A,B) | (A,B) <- append(A, B, [1,2,3])} # reverse β all four splits:
# {([1,2,3],[]), ([1,2],[3]),
# ([1],[2,3]), ([],[1,2,3])}
Reverse-mode list unification β splitting a list given only the result β is the one thing here that pattern matching alone cannot do. It comes from the relation engine, not from the array.
Because the cons tail may be a call (July 2026), a recursive constructor reads the same way it destructures:
func lmap(_, []) [[]]
func lmap(f, [h | t]) [[f(h) | lmap(f, t)]]
lmap(func(x) [x * x], [1, 2, 3]) # β [1, 4, 9]
func lapp([], ys) [ys]
func lapp([h | t], ys) [[h | lapp(t, ys)]]
lapp([1, 2], [3, 4]) # β [1, 2, 3, 4]
func ins(x, []) [[x,]]
func ins(x, [h | t]) [ if x <= h then [x | [h | t]] else [h | ins(x, t)] ]
func isort([]) [[]]
func isort([h | t]) [ins(h, isort(t))]
isort([5, 2, 8, 1, 9, 3]) # β [1, 2, 3, 5, 8, 9]
The tail may be an identifier, a list, a call, a
property read, an index, or a whole comprehension. It may
not be an infix expression β [h | a + b]
still reads as a comprehension clause, which is where the genuine
ambiguity with a filter lives. The tail must also evaluate to a list:
[1 | 2] is rejected with "list-cons tail [.. | t] must
be a list, got INTEGER", so there are no improper lists or dotted
pairs.
Two traps worth naming:
first/rest/last, nothead/tail.headandtailare the homoiconicity AST accessors βhead(xs)on an array errors with "argument must be an AST value".first(xs)/rest(xs)are the list accessors, andrestdoes not deep-copy, so a recursivefirst/restdescent is linear.- In a
matcharm,[h]is a block, not a one-element array β it evaluates toh. Everywhere else (binding right-hand side, call argument, operand, function-body last expression)[h]is the array[h]. Write[h,]when you mean a one-element array: it reads as an array in every position.
For a mutable chain β sharing, cycles, O(1) splice β
use hash nodes rather than arrays; {val: v, lnk: nd} is a
node and hashes are reference types. See test_doubly_linked_list.ax
for a full doubly linked list with O(1) unlink from a bare node handle.
Note that next is a reserved word, so either spell the
field lnk/nxt or keep the conventional name
with the guarded form $next, which stores the key bare:
xs: none
xs: {$next: xs, value: 1}
xs: {$next: xs, value: 2}
l: xs
while l [ println(l.value) ; l = l.$next ] # `none` is falsy, so no != test
Unary dot fallback β
xs.sum β‘ sum(xs) β‘ xs's sum
On a slot-less value β Array, Tuple, String, Set, Range, scalars, Bytes, Bag, AST β a dot or possessive read whose name is not an accessor applies the named unary operation to the receiver. One name, three positions, for the whole unary vocabulary:
xs: [3, 1, 4, 1, 5]
xs.sum # β 14 β‘ sum(xs) β‘ xs's sum
xs.sort # β [1, 1, 3, 4, 5]
xs.sort.reverse.first # β 5 chaining falls out of naming
{1, 2, 3}.sum # β 6 ; (1..5).sum β 15
"hello".upper # β "HELLO" ; " hi ".trim β "hi"
16.sqrt # β 4 scalars are slot-less, so eligible
double: func(x) [x * 2]
7.double # β 14 user functions participate identically
bytes("abc").len # β 3 Bytes/Bag/AST get accessor parity free
The name resolves exactly as in function position β lexical
environment first (user bindings shadow builtins), then the builtin
table β and the accessor table above keeps precedence
(xs.len answers the count even if len is
rebound). Records and namespaces never fall back: a
hash keeps miss β null (data wins β d.sum
reads the key sum), entities and Concepts keep their
property-not-found errors, and modules stay name qualification
(math.sqrt is a member lookup, never
sqrt(math)). Because a miss on every eligible receiver was
already an error, the fallback only turns errors into answers.
The fallback is strictly unary. A multi-argument
builtin surfaces its own arity error (xs.push β "push
requires 2 argumentsβ¦"), and x.f(a) means
(x.f)(a), never f(x, a) β for multi-argument
calls use function position (contains(xs, 2)) or the pipe's
hole (xs |> contains(_, 2)). A name bound to a
non-callable errors; a name that resolves to nothing raises the familiar
miss error, which now ends "β¦, or any unary operation in scope".
Variadic operations are unary-callable, so they participate
(xs.max β 5, the fold reading; "abc".max β
"abc", max-of-one). The dot spelling has full
--vm parity.
Tuples
pair: (3, 4)
trip: ("Alice", 30, "engineer")
Sets
Sets are unordered collections of
unique elements, written with braces. Duplicates
collapse and order is irrelevant β {3, 1, 2, 2} is
{1, 2, 3}, and {1, 2, 3} == {3, 2, 1}. The
empty set is {} (equivalently set(),
emptyset, or β
); len(s) is the
cardinality.
{3, 1, 2, 2, 1} # β {1, 2, 3} (deduped, unordered)
{1, 2, 3} == {3, 2, 1} # β true (order-independent)
{} == emptyset # β true ; set() == β
β true
len({1, 2, 3}) # β 3
Unordered as a value, deterministic as a walk. A set
has no intrinsic order β that is what
{1, 2, 3} == {3, 2, 1} means. But anything that
walks a set has to pick some order, and Axioma guarantees it is
always the same one: the set's canonical order, which
is what first, nth, s[i], and
display all show. Every consumer agrees with them β every loop form
(foreach, repeat, loop),
map, filter, array(s), the folds
(reduce, foldl, foldr), and the
Enumerable verbs (take_while, sort_by,
min_by, group_by, flat_map,
β¦).
s: {30, 4, 100, 2, 77}
first(s) # β 2 (canonical order is NUMERIC for numbers)
array(s) # β [2, 4, 30, 77, 100]
reduce(func(a, x) [a + str(x)], "", s) # β "243077100" β the SAME on every run
This matters because a fold is order-sensitive: without the
guarantee, reduce over a set would return a different
answer each time you ran the program. Sets built by different insertion
orders are indistinguishable to every consumer, which is the point.
The order itself is numeric for numbers. Elements compare at their exact mathematical value, so the whole exact tower β Integer, Float, Rational β sorts as one line by magnitude, at arbitrary precision:
array({2, 10}) # β [2, 10] (not [10, 2])
array({1/2, 0.25, 2, 3/2}) # β [0.25, 1/2, 3/2, 2]
array({2^100, 5, 2^64}) # β 5, then 2^64, then 2^100 β by magnitude,
# at arbitrary precision (printed in full)
array({inf, 5, -inf}) # β [-Inf, 5, +Inf]
array({"b", "a", "c"}) # β ["a", "b", "c"] (strings by codepoint)
Non-numeric elements keep a stable order of their own, so a set
mixing types still walks the same way every time. The law is checkable β
axioma lib/laws/determinism.ax runs it as a property
test.
Converting β array(s) materializes a
set as an Array in canonical order, and set(xs)
deduplicates any collection into a Set. They join the bare type-name
coercions (str, int, float,
bytes).
tuple(c) completes the family, materializing any of them
as a fixed-arity Tuple. All three also answer the
no-argument call with their empty value β the explicit
spellings of the literals [], {} and
().
array({3, 1, 2}) # β [1, 2, 3]
set([1, 2, 2, 3]) # β {1, 2, 3}
tuple([1, 2, 3]) # β (1, 2, 3)
array(set([9, 1, 9])) # β [1, 9] (round trip)
array() # β [] (β‘ the literal; a FRESH array each call)
set() # β {} (β‘ β
)
tuple() # β ()
make_tuple is a different facility β it builds an AST
node, not a value.
Algebra β each operation has a word form and a glyph, and yields a set:
| Operation | Word | Glyph | a Β· b β |
|---|---|---|---|
| union | union |
βͺ |
{1, 2, 3, 4, 5, 6, 7} |
| intersection | intersect |
β© |
{3, 4, 5} |
| difference | difference |
\ |
{1, 2} |
| symmetric difference | symdiff |
β³ (also β β) |
{1, 2, 6, 7} |
a: {1, 2, 3, 4, 5}
b: {3, 4, 5, 6, 7}
a union b # βͺ β {1, 2, 3, 4, 5, 6, 7}
a intersect b # β© β {3, 4, 5}
a difference b # \ β {1, 2}
a symdiff b # β³ β {1, 2, 6, 7} (in exactly one of the two)
Relations & membership β each returns a
Boolean:
| Test | Word | Glyph | True when |
|---|---|---|---|
| membership | in |
β |
the element is in the set |
| non-membership | notin |
β |
the element is not in the set |
| subset | subset |
β |
every element of the left is in the right |
| superset | superset |
β |
the right is a subset of the left |
| proper subset | subsetneq |
β (β) |
subset and not equal |
| proper superset | supsetneq |
β (β) |
superset and not equal |
| equality | == |
β | same elements (order-independent) |
2 in a # β true ; 9 notin a β true
{1, 2} subset a # β true ; a superset {1, 2} β true
{1, 2} subsetneq a # β true (proper) ; a subsetneq a β false
2 β a and {1, 2} β a # β true (glyphs work everywhere the words do)
Powerset & Cartesian product:
powerset({1, 2}) # β all 4 subsets: {}, {1}, {2}, {1, 2}
{(x, y) | x <- {1, 2}, y <- {3, 4}} # Cartesian product via a comprehension β
# {(1, 3), (1, 4), (2, 3), (2, 4)}
See also. Set comprehensions and
quantifiers β {x | x <- s, p(x)},
forall / exists over a set β are covered in Β§7. The Ellipsis
sets subsection just below covers textbook and
infinite sets ({2, 4, 6, ...}) and the
built-in number sets β β β€ β β β β β β. Draw set
relationships as a diagram with venn(...) β see Β§23.
Ellipsis sets β
textbook {2, 4, ..., 100} /
{2, 4, 6, ...}
The ... ellipsis writes an arithmetic progression the
way a textbook does. A bounded form materializes a set; an open form (no
upper bound) is a lazy infinite set. The step is
inferred from the leading terms β a third term, if given, must confirm
it.
{2, 4, ..., 100} # β {2, 4, β¦, 100} β a 50-element set
{1, ..., 10} # step defaults to 1 β {1..10}
{10, 8, ..., 2} # descending (step β2)
B: {2, 4, 6, ...} # infinite (lazy)
first(B) # β 2 ; tenth(B) β 20 ; nth(B, 50) β 100
first(B, 5) # β {2, 4, 6, 8, 10}
100 in B # β true ; 7 in B β false
first((x | x <- B, x > 10), 3) # β [12, 14, 16] (a filtered lazy stream)
Integer progressions only (non-integer or non-linear β a clear error
pointing at an explicit generator like
{x | x <- range(...)}); bounded sets cap at 1,000,000
elements. For an ordered infinite sequence use this form or
infinite_set("naturals") β a bare naturals /
β generator enumerates unordered.
Pulling elements. first(s) is the first
term; the ordinals second(s) β¦ tenth(s) and
the general nth(s, k) give the k-th β on a finite
collection or an infinite set (tenth({2,4,6,...}) β 20).
last(s) errors on an infinite set (no last element).
Membership uses in: ellipsis and named sets test it exactly
and instantly, while a custom function-defined set
(infinite_set(func(n) [...])) does a bounded
generate-and-check (up to 100k terms, with an early exit once an
ascending sequence passes the target) β so
9 in infinite_set(func(n) [n * n]) is true
without ever running away.
The standard number sets. The chain β β β€ β
β β β β β is built in. The identifiers rationals /
reals / complexes and the glyphs
β / β / β are membership
sets:
3 in reals # true (an integer is realβ¦)
2.5 in reals # true ; 2.5 in rationals β false (a decimal reads as a real)
rational(1, 2) in rationals # true
complex(0, 1) in complexes # true ; complex(0, 1) in reals β false (nonzero imaginary part)
first(rationals, 5) # {0, 1, -1, 1/2, -1/2} (β is countable β enumerable)
first(reals, 5) # ERROR: β is uncountable β use a membership test instead
Membership is type-faithful: an Integer belongs to all
four; a RationalNumber to β/β/β; a Float reads
as a real, not a rational (decimals approximate reals β
assert rationality with a rational(p, q) literal); a
ComplexNumber belongs to β, and to β only when its
imaginary part is 0. β is countable, so first(rationals, n)
enumerates it (CalkinβWilf order, signs interleaved); β and β are
uncountable and so are membership-only (enumeration is a clean error).
infinite_set("rationals" | "reals" | "complexes") builds
the same sets.
Bags (multisets)
A Bag is an unordered collection in which the same value may appear multiple times. Bags sit between Sets (no duplicates) and Arrays (ordered duplicates):
| Order | Duplicates | Multiplicity | |
|---|---|---|---|
| Array | yes | yes | positional |
| Set | no | no | n/a |
| Bag | no | yes | counted |
words: bag(["the", "quick", "the", "fox", "the"])
count(words, "the") # 3 β multiplicity of an element
len(words) # 5 β total Ξ£ multiplicities
"the" in words # true
distinct(words) # {the, quick, fox} β underlying support set
Operators. Standard multiset algebra is wired infix:
b1 βͺ b2 # union β max counts per element
b1 + b2 # additive sum β counts add (independent observations)
b1 β© b2 # intersection β min counts
b1 - b2 # difference β clamped at zero
b1 == b2 # multiset equality
βͺ and + are genuinely different:
bag([a,a]) βͺ bag([a]) == bag([a,a]) but
bag([a,a]) + bag([a]) == bag([a,a,a]).
Comprehension iteration is over distinct elements
(the underlying Set support), since {...} cannot carry
multiplicities. Use to_array(b) to iterate every
occurrence.
As slot values. Bags can be slot values on Concepts;
the unify machinery uses additive sum as
the merge policy β two partial observations of the same entity combine
their evidence.
Typical use cases: word-frequency counts, inventory, voting tallies,
evidence merging. Bags are first-class in SETL, Z, B, VDM-SL, Smalltalk,
Python's Counter, C++ std::multiset, and Guava
Multiset; they fill the same role in Axioma.
Tests: tests/axioma/collections/test_bag_basic.ax, test_bag_operators.ax, test_bag_use_cases.ax, test_bag_slot_value.ax.
Dictionaries
({key: value} literals)
person: {name: "Anna", age: 24} # Unquoted keys
obj: {"first-name": "Bob", "age": 30} # Quoted keys
nested: {user: {address: {city: "NYC"}}}
person.name # "Anna" (dot access)
person["name"] # "Anna" (bracket access)
dict() # Empty dictionary ({} is the empty SET β
)
type(person) # "Dictionary"
person is Dictionary # true
A dictionary is a string-keyed
{key: value} map. Build one with the {...}
literal or, for the empty case, dict(). type()
and @ report "Dictionary", and
x is Dictionary classifies it.
Naming note. This type was previously named
ObjectMap(June 2026 standardized it toDictionaryand fully retired the old name βObjectMapis now an undefined word, not an alias).Dictis accepted as a short alias in type annotations (x :: Dict) andischecks, matching thedict()constructor, buttype()/@always report the canonicalDictionary.
Stacks
First-class stack data type. See Β§18.
s: a Stack
s push 1
s push 2
s pop # 2
Matrices, tensors & dataframes
Three numeric/data containers, constructed by builtins (no literal
form). All three are first-class types β type(v),
@v, and v is Matrix / is Tensor /
is DataFrame agree.
Matrices β 2-D, with linear-algebra infix operators:
m: matrix([[1, 2], [3, 4]]) # 2Γ2 from nested arrays
type(m) # β "Matrix"
m + matrix([[10, 20], [30, 40]]) # elementwise + / -
m * matrix([[5, 6], [7, 8]]) # MATRIX PRODUCT ; m * 10 β scalar scale
m ^ 2 # matrix power (integer exponent)
transpose(m) # rows β columns
det(m) # β -2 ; trace(m) β 5
reshape(matrix([[1, 2, 3], [4, 5, 6]]), 3, 2) # 2Γ3 β 3Γ2
zeros(2, 2) # 2Γ2 of 0s ; ones(2, 3) β 2Γ3 of 1s
solve(matrix([[2, 1], [1, 3]]), [5, 10]) # Ax = b β column vector (1, 3)
Two naming gotchas.
identityis the identity function (identity(42)β42), not an identity-matrix constructor β writematrix([[1, 0], [0, 1]]). And there is no matrix-inverse builtin (inverseis the relation/set inverse) β for linear systems usesolve(A, b)directly.
Tensors β n-dimensional generalization:
t: tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) # rank-3 from nesting
shape(t) # β [2, 2, 2] ; ndim(t) β 3
tensor([2, 3], 7) # shape + fill β 2Γ3 of 7s
tensor_reshape(tensor([1, 2, 3, 4, 5, 6]), [2, 3]) # β 2Γ3
squeeze(tensor([[1, 2, 3]])) # drop size-1 axes β vector [3]
expand_dims(tensor([1, 2, 3]), 0) # add an axis β 1Γ3
DataFrames β column-oriented tables (pandas-style):
df: dataframe({name: ["ada", "bob", "eve"], age: [36, 25, 30]})
type(df) # β "DataFrame"
df_select(df, ["name"]) # column projection
df_filter(df, func(row) [row["age"] > 26]) # row predicate β row is a Dictionary
df_sort(df, ["age"]) # ascending (optional 3rd arg: ascending?)
df_head(df, 2) # first rows ; df_tail(df, 1) β last rows
df_info(df) # schema: dtypes, non-null counts, memory
df_describe(df) # count / mean / std / min / quartiles / max
read_csv(path) loads a file into a
DataFrame; df_groupby(df, cols) buckets rows.
(For plain in-memory grouping of arrays/hashes, group_by in
Β§7 needs no DataFrame at
all.)
6. Operators & Control Flow
Arithmetic
2 + 3 2 - 3 2 * 3 6 / 3 7 % 2 2 ^ 10
100 // 7 10.0 // 3.0 # INT_DIV (floor)
Each binary operator has a symbolic form, a keyword form, and a prefix-builtin form. They produce identical AST and identical results β pick by readability:
| Operation | Symbolic | Keyword (infix) | Prefix builtin |
|---|---|---|---|
| Quotient (floor / trunc) | // |
div |
quotient(a, b) |
| Remainder | % |
mod / modulo |
remainder(a, b) |
| Both at once | β | β | divmod(a, b) β (q, r) |
100 // 7 # β 14 (symbolic)
100 div 7 # β 14 (keyword)
quotient(100, 7) # β 14 (prefix)
100 % 7 # β 2 (symbolic)
100 mod 7 # β 2 (keyword)
100 modulo 7 # β 2 (keyword longhand)
remainder(100, 7) # β 2 (prefix)
divmod(100, 7) # β (14, 2) (combined β one division, both halves)
/ vs // for floats.
/ preserves operand kind (int/int β int truncated;
float/float β float real value); // always returns an
integer-valued result via Go-native truncation (ints) or
math.Floor (floats). The two differ for all float
cases and for negative-result float cases in particular:
100.0 / 7.0 # β 14.2857β¦ (real value)
100.0 // 7.0 # β 14 (floor)
-7.0 / 3.0 # β -2.333β¦ (real value)
-7.0 // 3.0 # β -3 (floor toward -β, NOT -2)
mod / modulo / div are
soft keywords: lexed as plain identifiers and
recognized as infix operators only when they sit between two expressions
on the same line. Variables, hash keys, and function names with these
spellings keep working (mod: 99, h.div).
Comparison
== != < > <= >=
3 β€ 5 # glyph twin of <= (β₯ for >=; β for !=; slanted β©½ β©Ύ also lex)
0 β€ x < 10 # ordering comparisons CHAIN: β (0 β€ x) and (x < 10)
Ordering comparisons (< <=
> >= and their glyphs) chain like the
mathematician's interval notation β a < b < c
desugars to (a < b) and (b < c), reusing the middle
operand. Equality (== !=) deliberately does
not chain, preserving the (a == b) == c
boolean idiom. Digraphs: `leq / `geq.
Compound assignment
i: 5
i += 1 # β‘ i = i + 1 (also -= *= /=)
s: "log"
s += ":entry" # + concatenates strings/arrays, so += inherits that
c.n += 1 # property, index, and dereference targets work too
xs[2] += 7 # β the same target set the `=` form supports
target op= value is statement-level
sugar: it desugars at parse time to the same AST as
target = target op value (find-or-update), so the evaluator
and --vm inherit semantics byte-for-byte. It is not an
expression β y: (x += 1) is a parse error, so there is no
value-of-assignment ambiguity. Only += -= *= /= exist;
%= and **= error with a hint naming the
rewrite. There is deliberately no
++/-- (the C-family increment):
adjacent ++/-- are parse errors with a rewrite
hint β before this, --i silently parsed as double negation
-(-i) and returned i unchanged. Spaced forms
keep their math meaning: -(-x) and - -x are
still double negation. For a pure, value-returning step use
succ(n) / pred(n) (Β§22).
Logical (auto-dispatched on operand type)
and or not implies iff xor
When operands are multi-valued logic instances (Belnap, Intuit3, Εukasiewicz, Kleene), the operators dispatch automatically. See Β§9.
xor is exclusive or β true when exactly one
side is true (iff is its negation, i.e. XNOR). The glyph
β» canonicalizes to xor (byte-identical, VM
parity): true β» true β false. Digraphs:
`xor / `veebar.
~ is not
negation β the tilde family map
Negation is spelled not / Β¬ only. Prefix
~x was retired (July 2026): it was an undocumented alias of
not, and every C-lineage reader (C, Python, JS, Lua) reads
~5 as bitwise NOT (= -6), so
the valid reading was a silent cross-language trap. ~5 is
now a loud SyntaxError whose hint names the rewrites. Likewise
a ~= b (Lua/MATLAB's not-equal) is a SyntaxError with a
hint β write a != b (or a β b). It will not
become an alias: =~ is the regex-match
operator, and mirror-image operators one transposition apart with
unrelated meanings would turn typos into silent bugs.
Where ~ does appear: the defeasible markers
(rule~, defines~, unify~,
boundary~) and defeasible rule arrows (<~~,
~~>), and the Perl-borrowed regex pair (=~
match / !~ non-match). Bitwise NOT is
bit_not(x) β bit_not(5) β -6,
bit_not(0) β -1 β alongside band,
bor, bxor, bshl,
bshr.
nand
/ nor β the Sheffer stroke βΌ and Peirce arrow
β½
The two singly functionally-complete connectives (Post's theorem:
each alone defines Β¬, β§, β¨ β the company here is Mathematica's n-ary
Nand/Nor and APL's primitive
β²/β±). Two spellings each:
true βΌ false # β true NAND glyph infix: Β¬(p β§ q)
false β½ false # β true NOR glyph infix: Β¬(p β¨ q)
nand(true, true) # β false prefix builtin
nand(a, b, c) # n-ary: Β¬(a β§ b β§ c), like Mathematica's Nand
nor([p1, p2, p3]) # collection fold β Wittgenstein's N-operator
# (TLP 5.502): true iff EVERY proposition is false
(p βΌ p) == (not p) # β true Β¬ recovered from NAND alone
nand/nor are not keywords
β a local nand: func(p, q) [...] binding shadows the
builtin, so existing definitions keep working. The glyphs sit at
xor/or precedence and are MVL-dispatched (each
logic computes Β¬ββ§ / Β¬ββ¨ with its own tables: om βΌ true β
Ξ©, belnap("both") βΌ belnap("true") stays a
glut). Digraphs: `nand / `barwedge,
`nor / `barvee. Empty folds:
nand([]) β false, nor([]) β
true (the negated β§/β¨ identities). βΌ/β½ chains are not
associative with themselves β parenthesize, or use the n-ary builtin for
Β¬(a β§ b β§ β¦).
therefore /
β΄ β the conclusion connective
p therefore q (glyph p β΄ q) draws a
conclusion, marking it with [logical] grounding at full
strength β distinct from the truth-functional implies. The
glyph and the word are byte-identical:
p β΄ q # prints: p β[logical] q (strength: 1.00)
p therefore q # identical to the glyph form
Type β΄ directly, with the backtick digraph
`therefore / `qed / `thus, or via
the REPL \therefore+Tab expansion.
Set operations
union intersect difference symdiff in notin
subset superset subsetneq supsetneq # β β β β word twins
βͺ β© \ β³ β β β β # glyph forms
β β β β β β # subset family (neq = proper/strict)
Conditional
if x > 0 then "positive" else "non-positive"
if x > 10 then [
println("big")
] else if x > 0 then [
println("small")
] else [
println("non-positive")
]
Bracketed if-bodies are blocks, not arrays. Inside
then [...] and else [...], a single-expression
body returns the expression's value, not a single-element array β
if cond then [42] else [99] returns 42, not
[42]. Comma-separated [1, 2, 3] and empty
[] still parse as array literals everywhere. The same
contextual rule applies to match β¦ | pat => [x]
arms.
What counts as true
Only the bottoms are false. false and
none are falsy; a typed multi-valued truth value is falsy
when its logic does not designate it. Every other value is
truthy β including 0, 0.0, "",
[], {} and dict().
if 0 then "T" else "F" # β "T" β zero is a number, not a bottom
if "" then "T" else "F" # β "T"
if [] then "T" else "F" # β "T"
if {} then "T" else "F" # β "T" β β
is a set, not falsehood
if none then "T" else "F" # β "F"
if β₯α΅ then "T" else "F" # β "F" β undesignated
if β€β₯α΅ then "T" else "F" # β "T" β a glut IS designated
empty?([]) # β true β this is how you ask about emptiness
This is the SETL/Lisp policy rather than the C/Python/JavaScript one,
and the reason is specific to Axioma. β
is a first-class
mathematical object here, so a rule making the empty set false would
leave set theory unable to state its own theorems:
if (A intersect B) would silently mean "if they
intersect" β a different proposition from the one written.
Emptiness is a question you ask with empty?, which says
what it means.
One consequence worth internalizing: truthiness is not a "did
it work?" check. A caught Error is
truthy β handle it with
try/otherwise/??, not
if. (none and om are both falsy,
and a total read's absence is none, so you rarely need to
if-test an error at all β but is_error(e) is
the explicit test when you do.)
Truthiness is a total function of the value, and
if, while, filter, comprehension
guards, the postfix if/unless modifiers and
the logical operators all read the same one β in both runtimes. That is
not a free-standing promise: before July 2026 the interpreter decided
Booleans by comparing against its cached
true/false objects, so a Boolean produced by,
say, an FFI call was truthy whatever its value, and the same program
could branch differently under --vm.
Loops
Three keywords. for is an alias of foreach,
and loop of repeat, so there are three
constructs and five spellings.
# ββ while β condition-driven; the counter is YOURS and outlives the loop
n: 1
while n <= 5 [ println(n) n = n + 1 ]
while (n < 10) [ n = n + 1 ] # parens optional
# ββ foreach β‘ for β source-driven, and the variable is SCOPED
foreach v in [10, 20, 30] [ println(v) ]
for v in [10, 20, 30] [ println(v) ] # same keyword
for v at i in xs [ println(i, v) ] # 1-based index; three spellings
for v in xs with index i [ println(i, v) ] # of one idea
for i, v in xs.indexed [ println(i, v) ]
foreach [x, y] in pairs [ println(x + y) ] # destructuring; three spellings
foreach (x, y) in pairs [ println(x + y) ]
foreach x, y in pairs [ println(x + y) ]
# ββ repeat β‘ loop β source- OR condition-driven, one optional variable
repeat 5 [ println("hi") ] # count only, no variable
loop 5 [ println("hi") ] # same keyword
repeat i <- xs [ println(i) ] # bound variable over a source
repeat i <- 3 [ println(i) ] # a count IS a source β 1-based
repeat i 3 [ println(i) ] # older no-arrow spelling
repeat xs [ println("tick") ] # bare source, no variable
n: 0
repeat [n < 3] [ n = n + 1 ] # PRE-test block condition
n: 0
repeat [ n = n + 1 ] until n >= 3 # POST-test; body runs β₯ once
# `repeat` is an expression too, so it can sit on the right of a binding β
# same headers, and the bound value is a by-product only.
r: repeat i <- 1..10 by 3 [ println(i) ]
Loop sources β the same set in for β¦ in
and repeat β¦ <-:
for v in [1, 2, 3] [ β¦ ] # array
for v in (1, 2, 3) [ β¦ ] # tuple
for v in {3, 1, 2} [ β¦ ] # set β ONE canonical order, numeric
for v in bag([3, 1, 2, 1]) [ β¦ ] # bag β a multiset, WITH multiplicity
for c in "abc" [ β¦ ] # string, by character
for k in keys(d) [ β¦ ] # dictionary keys, sorted
for k, v in items(d) [ β¦ ] # dictionary pairs, destructured
for d in Day [ β¦ ] # enum, in declaration order
for x in SomeConcept [ β¦ ] # a concept's instances
for i in 1..3 [ β¦ ] # range, inclusive
for i in 1..<3 [ β¦ ] # exclusive end
for i in 3..1 [ β¦ ] # descending, from the operands
for i in 1..10 by 3 [ β¦ ] # `by` = positive MAGNITUDE
for i in 10..1..-3 [ β¦ ] # `..n` = SIGNED step
for i in range(1, 10, 3) [ β¦ ] # the call form β the same Range value
for i in 1.. [ if i > 3 then break ] # OPEN range β infinite, so break
for v in (n * n | n <- 1..) [ β¦ ] # lazy comprehension
for i in iterate(func(v) [v * 2], 1) [ β¦ ] # arbitrary-step stream
for i in take_while(func(x) [x < 20], iterate(func(v) [v * 2], 1)) [ β¦ ]
break and continue work in every loop form,
and return escapes the whole function from inside one.
repeat-style loops without until are pre-test
(the count or condition is checked before each pass); the
until form is post-test.
A loop scopes what the loop binds.
for/foreach and every source form of
repeat bind the loop variable themselves, so it does not
outlive the loop. The condition-driven forms β while,
repeat [cond] [β¦], repeat [β¦] until cond β
bind nothing: the counter is yours, lives in your scope, and survives. A
body still reads and updates enclosing names normally, so accumulator
loops are unaffected β that is a body assignment, which is
find-or-update. The loop variable itself is a binder, so one
that matches a pre-existing outer name shadows it rather than writing it
(see Β§Scoping & shadowing).
A lazy stream is what a lazy comprehension, an open
range, and iterate(f, x) produce (@g β
"Generator"). The loop pulls one element at a time, so an
unbounded source is fine as long as the body breaks β or as
long as take_while bounds it, which stops at the first
failing element. iterate(f, x) without a count is how you
get a counter whose step is an arbitrary function while keeping it
scoped to the loop: for i in 1..n steps by a constant, and
while takes any step but leaves its counter bound after the
loop ends.
A stream is walked once. Stopping partway and
looping again resumes where you left off; walking a stream that
has already been drained (by force, by first,
or by a loop that ran to the end) raises a catchable error rather than
silently running zero iterations.
A complete, executable inventory of every form above β each one
asserted, so it cannot rot β lives in
tests/axioma/showcase/loops.ax.
Statement branches β if c then break,
if c then a: 0. A branch may be a bare
statement: break, continue,
return, or a binding. Each is sugar for
the block form the brackets already spelled (then [break]),
built by the same parse, so the two spellings are the same AST and
cannot drift. Both arms take all of them:
while true [
if done then break # β‘ if done then [break]
if skip then continue
if x < 0 then "note it" else break # the else arm too
]
if a < 0 then a: 0 # bindings too β `=` spells it as well
if a < 0 then a = 0 else a: 100 # β¦in either arm
if hit then tally[k] += 1 # every assignment target the brackets
if hit then rec.total: 0 # accept: multi-assign, index, property,
m, n: 1, 2 # compound
if swap? then m, n: n, m
Two things to know. A binding branch scopes a fresh name to
the block, exactly as if c then [z: 1] always did
β so if c then z: 1 on a new z binds
something that vanishes at the branch's end, while an existing
name is found-and-updated (the common case). And a postfix guard is
return-only: return -1 if x < 0 works,
break if x < 0 does not (write
if x < 0 then break).
Indexed iteration
β at i, with index i,
.indexed
When a loop needs the element and its position,
three equivalent surfaces attach a 1-based counter (element-first, so
the common element-only loop stays the short one). for and
foreach are one keyword β every form below works with
either spelling:
fruits: ["fig", "plum", "lime"]
for f at i in fruits [ println("${i}. ${f}") ] # at-clause
for f in fruits with index i [ println(i) ] # with-index clause
for i, f in fruits.indexed [ println(f) ] # destructure the pairs
for i, f in enumerate(fruits) [ println(i) ] # builtin spelling
pairs: [(10, "x"), (20, "y")]
for v, tag at i in pairs [ β¦ ] # composes with destructure: v, tag
# from the element, i from the counter
for n at i in 2..100 by 2 [ # any iterable β ranges included; break
if i == 3 then [break] # and continue see the same i
]
The counter is always 1-based and counts iterations
(for a String it counts runes; for a Set it follows the canonical sorted
order β the same order .indexed and enumerate
report). at and index are soft
keywords, recognized only in these clause positions β variables
named at or index, and loops like
for at in xs, keep working. Writing both clauses at once
(for x at i in xs with index j) is a pointed error: pick
one.
An open-ended range makes the classic search-until loop:
s: 0
foreach k in 1.. [ # 1, 2, 3, β¦ forever β until break
s = s + k
if s > 20 then [break]
]
s # β 21
Like all loops, the indexed forms are evaluator-only under
--vm (the pre-existing boundary). Tests:
tests/axioma/control/test_indexed_iteration.ax,
tests/axioma/properties/test_indexed_accessor.ax.
Operator precedence (high β low)
- Function application:
f(x) ^(exponent),not,&(address),*(deref)- unary
-(negation) *,/,%,//,mod,modulo,div+,-- Set:
union,intersect,difference - Comparison:
<,>,<=,>=,==,!=,subset,in,is andor,xor/β»,βΌ(nand),β½(nor)implies,iff|>/|?>(forward pipe & error-propagating pipe β loosest; left-associative)
Associativity. ^ / ** /
.^ (exponentiation) are right-associative,
matching standard math convention: 2 ^ 3 ^ 2 is
2 ^ (3 ^ 2) = 512, not
(2 ^ 3) ^ 2 = 64. Every other infix operator
(+, -, *, /,
%, β¦) is left-associative.
Unary minus binds looser than ^.
Negation sits below exponentiation in the table above, so
-2 ^ 2 parses as -(2 ^ 2) = -4,
not (-2) ^ 2 = 4 β again the math-textbook
reading. Parenthesize the base ((-2) ^ 2) to negate first.
It still binds tighter than *//, so
-2 * 3 is (-2) * 3.
Negative integer exponents stay exact.
int ^ -n returns the exact Rational 1/int^n
(an Integer when integral), the same way / keeps
1/2 exact β so 2 ^ -3 β 1/8,
2 ^ -1 β 1/2, 1 ^ -5 β
1. (0 ^ -3 is a division-by-zero error.) Use a
Float base/exponent or pow(...) if you want a Float result
instead.
A fractional exponent is a root, and exactness follows the
operands. A rational exponent p/q takes the
q-th root. When that root is exact you get an exact answer,
at arbitrary precision β no detour through float64:
4 ^ (1/2) # 2 β an Integer, not 2.0
(8/27) ^ (1/3) # 2/3 β numerator and denominator each rooted
27 ^ (2/3) # 9
4 ^ (-1/2) # 1/2 β negative exponent inverts, as ever
(2^100) ^ (1/2) # 1125899906842624 (= 2^50, exactly)
When no exact root exists, ^ still answers β as a Float,
so it is never a dead end:
2 ^ (1/2) # 1.4142135623731 β β2 is irrational
This is the same exactness rule + and *
follow: two exact operands can give an exact result; one inexact
operand makes the result inexact. So the exponent's spelling
decides the type, and the two spellings still agree on the
value:
4 ^ (1/2) # 2 Integer β exact exponent
4 ^ 0.5 # 2 Float β inexact exponent
4 ^ (1/2) == 4 ^ 0.5 # true β same number either way
A negative base has no real power at a non-integer
exponent. It raises a catchable error rather than producing
NaN:
(-1) ^ 0.5 # ERROR: negative base has no real power at a non-integer exponent: -1^0.5
(-2) ^ 3 # -8 β an INTEGER exponent is fine, whatever the sign
(-2.0) ^ 2.0 # 4
The rule keys on the exponent, not on the sign of the base
alone. Erroring is deliberate: a NaN would flow silently
through every later computation, and the neighbouring spelling
sqrt(-1) has always errored β two spellings of one question
should not disagree about whether it has an answer.
Floats keep IEEE semantics where the two systems differ:
0.0 ^ -1.0 is +Inf, while the exact
0 ^ -1 is a division-by-zero error. The exact tower answers
with mathematics; Float answers with IEEE.
The complex plane is the explicit opt-in.
sqrt(-1) and (-1) ^ 0.5 stay real-domain
errors, and both name the way through:
sqrt(complex(-1, 0)) # i
complex(-1, 0) ^ 0.5 # i β the same value; the two spellings agree
Complex sits at the top of the numeric
tower: every other numeric type embeds into it, so mixed
arithmetic just works, and ^ is exact at integer
exponents.
complex(3, 4) + 1/2 # 3.5 + 4i β Rational embeds
complex(3, 4) + byte(2) # 5 + 4i β so does Byte
complex(0, 1) ^ 2 # -1 β exactly, not -1 + 1.2e-16i
conjugate(complex(3, 4)) # 3 - 4i
-complex(1, 2) # -1 - 2i
sqrt, exp, log,
sin and cos accept a Complex
alongside the reals. The embedding runs through float64, so
exactness stops at the complex boundary β
exact?(complex(1, 0)) is false, and
complex(0, 1) + 1/3 is inexact. Keeping exactness inside
the plane would need exact Gaussian rationals, which Axioma does not
have.
Forward pipe |>
The forward pipe threads a value into a function call, so a chain of
transformations reads left-to-right in the order it
runs β instead of the inside-out nest that
map/filter/reduce otherwise
force:
xs |> map(g) |> filter(p) |> reduce(f, 0) # β‘ reduce(f, 0, filter(p, map(g, xs)))
21 |> double # bare name β double(21) β 42
The piped value is appended as the last argument β
Axioma's higher-order functions are function-first / collection-last
(map(fn, coll), filter(pred, coll),
reduce(fn, init, coll)), so the everyday chain just works.
For the collection-first builtins
(str_join, first, push), an
explicit _ hole says where the value lands:
["a", "b", "c"] |> str_join(_, "-") # β‘ str_join(["a","b","c"], "-") β "a-b-c"
myset |> first(_, 3) # β‘ first(myset, 3)
| Form | Means | |
|---|---|---|
x |> f |
f(x) |
bare name |
x |> f(a, b) |
f(a, b, x) |
last-arg insertion |
x |> f(a, _, b) |
f(a, x, b) |
_ hole |
a |> f |> g |
g(f(a)) |
left-associative |
x |> h.fn |
h.fn(x) |
function stored in a slot |
|> has the loosest precedence (so
20 + 1 |> double is (20+1) |> double),
is left-associative, and composes in expression position β inside
function bodies, if conditions, bindings, and across
multiple lines (a leading |> continues the
pipeline):
result: data
|> filter(func(r)[r.valid])
|> map(transform)
A natural fit for Axioma is post-processing a relational query or
rendering with the *form family:
{Y | Y <- ancestor("tom", Y)} |> sort |> first(_, 3)
txns |> filter(func(t)[t.amount > 50]) |> tableform |> println
Error-propagating pipe
|?>
|?> is the safe sibling of
|>: it short-circuits on a failed,
absent, or undetermined value. If a stage yields an Error
(e.g. from try(...) / error(...)),
none, or om, the remaining stages are skipped
and that value becomes the result β railway-oriented error flow without
nested ifs:
input |?> parse |?> validate |?> compute # stops at the first failing stage
5 |?> double |?> double # β 20 (nothing fails)
5 |?> boom |?> double # β the boom Error (double is skipped)
5 |?> nada |?> double # β none (double is skipped)
0 |?> double # β 0 (0 is a valid value, not a failure)
It shares everything else with |> β last-argument
insertion, the _ hole, left-associativity, dotted callables
β and differs only in the short-circuit. A plain value (even a falsy one
like 0 or "") is not a failure and
flows through; only Error / none /
om stop the pipeline. Use |> when every
stage always succeeds, |?> when a stage may fail and you
want the failure to propagate.
7. Set Theory & Comprehensions
Comprehensions
{x * 2 | x <- {1, 2, 3, 4, 5}} # {2, 4, 6, 8, 10}
{x | x <- range(1, 10), x mod 2 == 0} # {2, 4, 6, 8, 10}
{(x, y) | x <- {1, 2}, y <- {3, 4}} # {(1,3), (1,4), (2,3), (2,4)}
The generator clause is target <- source β or, in the
set-builder slot described below, target in source /
target β source.
British/ISO colon separator
Math texts write set-builder two ways: {x | P(x)}
(American) or {x : P(x)} (British/ISO). Set comprehensions
accept both separators β the two forms are identical down to the
AST:
{x : x <- range(1, 10), x mod 2 == 0} # same set as the pipe form
{2 * k : k <- range(1, 5)} # {2, 4, 6, 8, 10} β the {2k : k = 1..5} style
{(x, y) : x <- {1, 2}, y <- {3, 4}} # multi-generator works too
Hash literals are unaffected: {k: v, ...} stays a hash β
the colon is read as a comprehension separator only when a generator
clause (target <- source, or a gated
target β source) provably follows it. The colon form
applies to set comprehensions only; dict comprehensions
keep | (their head already uses :), and list
comprehensions keep | (inside [...] a colon
means a block binding).
ISO membership generators
β {x : x β U, P(x)}
Real ISO/British texts put membership in the binder slot.
in / β is accepted as a generator alias for
<- under one rule: the clause
x in S is a generator iff x is not already
bound in the comprehension and occurs in the head; otherwise it keeps
its membership-filter meaning. This makes the textbook form
executable verbatim while leaving every membership test untouched:
{x : x β {1, 2, 3, 4}, x > 1} # {2, 3, 4} β full ISO form
{x | x in range(1, 10), x mod 2 == 0} # word form, pipe separator
[x * 2 | x in [1, 2, 3]] # lists too β [2, 4, 6]
{(x, y) : x β {1, 2}, y β {8, 9}} # multi-generator Cartesian
{a + b : (a, b) β pairs} # tuple-destructure target
# Membership FILTERS keep their meaning (the gate):
{x | x <- s, x in t} # x bound β filter (s β© t idiom)
{c | c <- cs, g in c.allies} # g is outer/global β filter
{x | x <- s, x β t} # β never converts
[x for x in xs if x in t] # Python if-clause: always a filter
Works in all four comprehension flavors (set / list / dict / lazy),
with both spellings (in, β) and both
separators (|, :). One reinterpretation to
know about: the hash {x: x in s} β the same name
as key and membership subject β now reads as a comprehension; write
{x: (x in s)} (parenthesized value) or quote the key to
keep the hash.
Relational comprehensions (Prolog-style)
{X | X <- parent(X, _)} # All parents
{(X, Y) | parent(X, Y)} # All parent-child pairs
{X | X <- parent(X, _), age(X, A), A >= 18} # With filter
Concept-extent comprehensions
When the iterable is a bare concept name, comprehension iterates the
concept's auto-maintained Instances map:
usa: a Country {}
china: a Country {}
{X | X <- Country} # All countries
Tag-filter comprehensions
Filter relational-source comprehensions by epistemic grounding tag β in the set, list, dict, multi-variable, and multi-generator forms:
{X @theorem | X <- derived_relation(X, _)} # Theorems only
{X @conjecture | X <- flies(X)} # Defeasibly derived
[X @axiom | X <- parent(X, _)] # list form
{X: 1 @datum | X <- parent(X, _)} # dict form
{(X, Y) @axiom | rel(X, Y)} # multi-variable form
{(X, Y, n) @axiom | (X, Y) <- rel(X, Y), n <- [1, 2]} # multi-generator
Accepts @axiom, @postulate,
@theorem, @conjecture,
@hypothesis, @datum, @canceled,
@all, @* (@standalone is a legacy
alias of @datum). The tag filters facts drawn from
relational sources by their stored grounding. On a
non-relational source β a plain array/set, a concept extent
(X is Country), or a chain query
(rel1(...) and rel2(...)) β the tag is inert: those
elements carry no per-fact grounding to filter on.
Dict comprehensions
Produce a hash by emitting a key/value pair per iteration. Pipe form and pipe-less form are both supported:
xs: [1, 2, 3, 4, 5]
{x: x * x | x <- xs} # {1:1, 2:4, 3:9, 4:16, 5:25}
{x: x * x | x <- xs, x > 2} # {3:9, 4:16, 5:25}
{x: x * x for x in xs if x > 2} # pipe-less form, same result
Walrus bindings (compute once, reuse)
Bind a name mid-clause to avoid recomputing. The canonical form is
:, the same operator used for ordinary value binding:
{y | x <- xs, y: f(x), y > 0} # bind y once, filter on it
Bindings are iteration-local β they do not leak out of the comprehension (unlike Python's walrus, which escapes into the enclosing function scope).
A comprehension binding clause is written
y: expr; there is noy := exprorlet y = exprform.
Multi-filter folding
Multiple bare-expression filters in one comprehension are folded with
and:
{x | x <- 1..10, x > 3, x < 8, x % 2 == 1} # {5, 7}
Keyword aliases & the pipe-less form
Axioma also accepts for x in iter and
if cond as aliases for <- and the bare
filter inside the standard pipe form, plus a fully pipe-less form for
all three comprehension flavors (so a comprehension copied from Python
pastes in unchanged):
{x * 2 | for x in xs, if x > 2} # keywords inside pipe form
[x * 2 for x in xs if x > 2] # pipe-less list comp
{x * 2 for x in xs if x > 2} # pipe-less set comp
{x: x * x for x in xs if x > 2} # pipe-less dict comp
The first generator may use either for x in iter or
x <- iter. Subsequent clauses are equally flexible.
Deferred values β
lazy, force, memoize
lazy <expr> defers a computation and hands you the
thunk itself β a first-class value that runs at most
once, when you force it.
t: lazy (6 * 7) # nothing has run yet
force(t) # β 42 computed nowβ¦
force(t) # β 42 β¦and memoized: not recomputed
This is the expression twin of the
declare binding:
| Form | Kind | Forced by |
|---|---|---|
declare x = e |
binding β transparent | reading x (automatic) |
t: lazy e |
expression β a first-class value | force(t) (explicit) |
The difference is what you can do with it. A declare
binding has already forced itself by the time it reaches an argument; a
lazy value can be passed, stored, and
returned still unevaluated β so a computation that is never
needed never runs at all:
pick: func(flag, thunk) [if flag then force(thunk) else "skipped"]
pick(false, lazy slow()) # β "skipped" β slow() never ran
Thunks are ordinary values, so they compose:
ts: [lazy (1 + 1), lazy (2 + 2)]
map(force, ts) # β [2, 4]
force covers the whole family and is
idempotent: a thunk computes (once), a generator drains, and anything
else comes back unchanged β R7RS Scheme's rule, which is what makes
force safe to map over mixed data.
memoize(f) is the per-call companion.
Where lazy defers one expression, memoize
caches every call, keyed by argument value:
fast: memoize(slow)
fast(9) # computes
fast(9) # cached β slow() is not called again
lazyis a soft keyword β it is still usable as an ordinary name (lazy: 5,f(lazy)). It binds at prefix precedence, solazy 1 + 2is(lazy 1) + 2; parenthesize to defer the whole expression.lazyanddeclareare both evaluator-only β rejected at compile time under--vm.
Lazy generator expressions
Comprehensions wrapped in parens produce a lazy
Generator instead of materializing. The source is pulled
one element at a time on demand.
g: (x * 2 | x <- [1, 2, 3, 4, 5]) # Axioma pipe form
g: (x * 2 for x in [1, 2, 3, 4, 5]) # pipe-less form
g: (x + 1 | x <- xs, x > 15) # with filter
g: (x + 1 for x in xs if x > 15) # Python with filter
Driver builtins:
| Builtin | Effect |
|---|---|
first(gen, n) |
Pull the first n elements (the
memorable spelling β the same verb works on arrays,
strings, sets, and infinite sets). |
first(gen) |
Pull one element (the first). |
force(gen) |
Pull all remaining elements into an Array. (Same verb
as force on a lazy thunk β see above.) |
gen_take(n, gen) |
Pull first n elements β the explicit, lower-level alias
of first(gen, n). |
gen_drop(n, gen) |
Advance past n elements (mutates gen in place, returns
it). |
gen_next(gen) |
Pull one element. Returns Ξ© when exhausted. |
Prefer first(gen, n) β it's the same
"first n" verb you already use for arrays and infinite sets. The
gen_* prefix exists only because take is a
reserved keyword (natural-language selective import) and
drop is the Stack op.
g: (x | x <- [1, 2, 3, 4, 5])
first(g, 3) # [1, 2, 3] (same as gen_take(3, g))
force(g) # [4, 5] β remainder after partial take
Phase 2b β full multi-clause surface. Lazy comprehensions now accept the same clause vocabulary as eager list/set/dict comprehensions:
# Multi-generator (Cartesian, streamed β inner is re-eval'd each outer step)
pairs: (x * y | x <- [1, 2, 3], y <- [10, 20])
py_pairs: (x + y for x in [1, 2] for y in [100, 200])
# Walrus binding (`:` β the canonical form)
g: (s | x <- xs, s: x * x)
# Tuple destructure in first OR subsequent generators
dest: (n + 1 | (n, _label) <- tagged)
lab: (s + ":" + name | (s, name) <- pairs)
# Concept-extent source β bare concept name OR is form
gdps: (c.gdp | c <- Country)
gdps2: (c.gdp | c is Country)
# Relational predicate source β a fact-store query as the iterable
kids: (c | c <- parent("John", c)) # single generator
gkids: ((p, g) | p <- parent("John", p), g <- parent(p, g)) # chained
Streaming semantics. Multi-generator lazy form
materializes inner sources once per outer combination (so the inner can
reference outer-bound names β same as the eager path). The outermost
source can still stream from a true Generator or
InfiniteSet. Inner positions cannot use
InfiniteSet β they'd re-materialize infinitely on each
outer advance. gen_take(N, ...) bounds total pulls,
naturally cutting the outer loop short when N is reached.
Relational sources. When a generator's iterable is a
relational predicate call (x <- parent(x, _)), the lazy
paths fall back to the relational machinery β eager evaluation of such a
call fails, so this is detected by probe. The relation extent is
bounded, so it is resolved once per entry; downstream pulls stay lazy.
Works in single-generator, chained, inner, and filtered positions. The
same bridge serves list and dict comprehensions (July
2026), and a bare relation name is a valid source in
every flavor ([x | x <- node],
{x: y | (x, y) <- parent}). Because a relation extent is
a set with no intrinsic order, materialization into an ordered
collection is deterministic by construction: elements
come out sorted (lexicographically by display form), so
[Y | Y <- parent("John", Y)] and
force((Y | Y <- parent("John", Y))) return the same
array on every run β no sort(...) wrapper needed.
Limitations. OrderBy and
Having clauses are accepted by the parser inside lazy form
but silently discarded β both are intrinsically eager (sorting requires
full materialization). For an ordered lazy stream,
force(gen) then sort, or sort eagerly first.
Tests: tests/axioma/comprehensions/test_lazy_generators.ax (Phase 2a β single gen), tests/axioma/comprehensions/test_lazy_generators_phase2b.ax (Phase 2b β multi-clause), tests/axioma/comprehensions/test_lazy_relational_source.ax (relational sources)
ORDER BY clause
List comprehensions accept an orderby clause that sorts
the result. Sets and dicts ignore orderby (they're
unordered by nature). Direction defaults to asc; add
desc to reverse:
[x | x <- xs, orderby x] # asc by default
[x | x <- xs, orderby x desc] # descending
[p.name | p <- people, orderby p.age] # sort by computed key
[x for x in xs orderby x desc] # pipe-less form works too
The sort key is evaluated in the iteration environment (where the loop variable refers to the source element), so referring to fields of the source element works as expected.
Aggregation:
group_by, items, keys,
values
Four builtins fill the SQL-style aggregation gap.
group_by(fn, coll) partitions a collection into a hash;
items(hash) exposes it as (key, value) pairs;
keys/values return the parts individually. All
three enumerators walk the hash in sorted key order β
the same canonical order println(h) shows β so repeated
calls (and keys/values/items
against each other) always agree.
orders: [
{country: "US", amount: 100},
{country: "UK", amount: 50},
{country: "US", amount: 200},
]
by_country: group_by(func(o) [o.country], orders)
# {"US": [{...}, {...}], "UK": [{...}]}
# Iterate the hash with tuple destructure + ORDER BY:
pairs: items(by_country)
counts: [(p[1], len(p[2])) | p <- pairs]
sorted: [pair | pair <- counts, orderby pair[2] desc]
# [("US", 2), ("UK", 1)]
Tuple destructuring in
<-
In any generator (first or subsequent), the iteration target can be a
tuple pattern instead of a single variable β and since July 2026 the
parentheses are optional, matching the loop form
(for i, e in β¦):
pairs: [(1, "a"), (2, "b"), (3, "c")]
# First-generator tuple destructure (canonical form)
[n + len(s) | (n, s) <- pairs] # list comp
{n: s | (n, s) <- pairs} # dict comp
{n * 2 | (n, s) <- pairs} # set comp
# BARE destructure β same meaning, no parens (all flavors, any clause)
[n + len(s) | n, s <- pairs]
xs: ["p", "q", "r"]
{e | i, e <- xs.indexed, i > 1} # β {"q", "r"} β the .indexed idiom
[e for i, e in enumerate(xs) if i > 1] # pipe-less Python spelling too
# Idiomatic with items() over a hash
by_country: group_by(func(o) [o.country], orders)
{k: len(g) | (k, g) <- items(by_country)}
Source elements must be Tuples or Arrays of the matching arity.
Mismatched arity is a hard error. The bare form commits only when a run
of two-plus names closes with <-/in, so a
bare-identifier filter
({x | x <- xs, flag, x > 1}) keeps its filter
meaning.
A call as the source works like any other iterable β
{(i, e) | (i, e) <- enumerate(xs)} binds by the pattern.
(Before July 2026 this specific shape β tuple head + tuple pattern + a
call source β was silently misread as a relation query over a
relation named enumerate and returned {};
genuine relation sources like
{(X, Y) | (X, Y) <- edge(X, Y)} still take the
relational path.) Tests:
tests/axioma/comprehensions/test_bare_destructure.ax.
Hash destructure in
<-
First-generator targets can also be a hash pattern
that binds named fields directly. Two surface forms β implicit binding
(var name = key name) and explicit rename (key: var):
people: [
{name: "Alice", age: 30, dept: "Eng"},
{name: "Bob", age: 25, dept: "Sales"},
{name: "Carol", age: 35, dept: "Eng"}
]
# Implicit binding
[name | {name, age} <- people, age >= 30]
# β ["Alice", "Carol"]
# Explicit rename
[(n, a) | {name: n, age: a} <- people]
# β [("Alice", 30), ("Bob", 25), ("Carol", 35)]
# Mixed implicit + rename in same pattern
[name | {name, age: a, dept} <- people, a > 27 and dept == "Eng"]
# pipe-less form
[name for {name, age} in people if age > 27]
{n for {name: n} in people}
{name: age for {name, age} in people}
# Lazy form
g: (name | {name, age} <- people, age > 25)
force(g) # β ["Alice", "Carol"]
# Combined with orderby + limit
[name | {name, age} <- people, orderby age desc, limit 2]
# β ["Carol", "Alice"]
Skip-on-missing-key. When a pattern key is absent from a source hash, that row is silently dropped β destructure acts as filter+extract in one step:
mixed: [{name: "Alice", age: 30}, {name: "Bob"}, {name: "Carol", age: 35}]
[n | {name: n, age: a} <- mixed]
# β ["Alice", "Carol"] (Bob has no `age` key β dropped)
Wrong-type elements (anything not an
ObjectMap hash) surface a runtime error.
Scope. Hash destructure is currently supported only
in the first generator position. Subsequent generators
(after a , in pipe form) use the single-var or
tuple-pattern forms β {...} in that position would be
ambiguous with set/hash filter expressions.
Multi-column ORDER BY
orderby accepts a comma-separated list of sort keys,
each with its own direction. Sort is lexicographic across columns:
[p.name | p <- people, orderby p.dept, p.age desc]
# Sorts by dept ascending; within each dept, by age descending.
HAVING clause β terminal filter
having EXPR is a terminal filter that runs in the
iteration environment (so it can reference the loop variable). Distinct
from , EXPR filters only in position and intent β HAVING
reads as "row filter after aggregate computation" by convention:
[o.amount | o <- orders, having o.amount > 70]
# Combined with orderby (having runs at iteration time, not after sort):
[o.amount | o <- orders, orderby o.amount desc, having o.amount > 70]
LIMIT / OFFSET clauses
SQL-style row caps. limit N keeps at most N output rows;
offset N skips the first N post-filter rows. Both apply to
all four comprehension flavors (list, set, dict, lazy) and to both
surface forms (pipe and pipe-less). They are terminal-ish: once seen,
only the other of the pair may follow.
# Basic
[x | x <- xs, limit 3] # first 3 rows
[x | x <- xs, offset 5] # skip first 5
[x | x <- xs, offset 5, limit 3] # paging: skip 5, then take 3
# Order is free β these are equivalent
[x | x <- xs, limit 3, offset 5]
# Combined with orderby (SQL convention β limit/offset run AFTER sort)
[x | x <- unsorted, orderby x desc, limit 5]
# Combined with orderby + having
[t.amount | t <- txns, orderby t.amount desc, having t.amount > 50, limit 2]
# pipe-less form (no commas between clauses)
[x * 3 for x in xs if x > 2 limit 4]
[x for x in xs offset 6 limit 2]
[x for x in unsorted orderby x desc limit 2]
# Lazy generator β limit caps total Next() pulls, offset skips post-filter rows
g: (x * 2 | x <- big_source, x > 100, offset 10, limit 5)
gen_take(3, g) # yields up to 3 elements (lazy stops emitting at limit anyway)
Soft keywords. limit and
offset are NOT reserved at the lexer level β they remain
ordinary identifiers usable as variable names, property names, and DSL
slot names (bindings.limit, {limit:number}).
The parser recognizes them as clause keywords only when they appear as
the first token of a comprehension clause.
Caveat. Inside a comprehension clause list, you
cannot use limit or offset as the LHS of a
filter expression. [x | x <- xs, limit > 5] parses
limit as the LIMIT clause keyword and chokes on
> 5. Parenthesize to disambiguate:
[x | x <- xs, (limit > 5)].
Validation. Both clauses evaluate to non-negative
integers. Negative or non-integer values surface as runtime errors.
limit 0 yields the empty result; offset N with
N greater than the source length yields the empty result.
Sets/dicts. Limit/offset DO apply (cap output cardinality), but the slice is taken in Go-map iteration order β non-deterministic. Use list comprehensions for deterministic paging.
Tests: tests/axioma/comprehensions/test_tier15_first_gen_multi_having.ax
Tests: tests/axioma/comprehensions/test_tier1_orderby_groupby.ax
Range generators
Any range expression is a valid generator source, and iteration
follows the range's own order (see Ranges in Β§4 β
direction, ..<, by steps):
[x * x | x <- 1..10] # inclusive 1..10 β [1, 4, 9, 16, β¦, 100]
[x | x <- 1..<10, x % 2 == 0] # half-open β [2, 4, 6, 8]
[x | x <- 10..1 by 3] # descending, stepped β [10, 7, 4, 1]
{x | x <- 1..100, prime?(x)} # set of primes β€ 100
first((x * x | x <- 1..), 5) # OPEN-ENDED range in a LAZY comprehension β
# streams on demand β [1, 4, 9, 16, 25]
[x | x <- 1..] # eager comprehension over 1.. β catchable
# Error (it would never finish)
An open range (1..) is the natural unbounded integer
source: use it in a lazy (parenthesized) comprehension
and pull with first(gen, k) / gen_take. For
non-arithmetic infinite sources ("primes",
"fibonacci", β¦) infinite_set(...) remains the
tool.
Intensional-class generators (Russell iota)
Define an intensional class once with
the <Concept> where <pred> and use it as a
generator source. Membership is computed by combining the base concept's
extent with the predicate restrictor:
adults: the Person where age >= 18
all_adult_names: [p.name | p <- adults]
This is the Russell-faithful form of "the things that satisfy P." Useful when the same restrictor predicate is consulted from several comprehensions β define it once, reuse by name.
The same question, many ways
Comprehensions, quantifiers, higher-order builtins, and the membership operator all answer overlapping questions. The cleanest illustration is the existential question β "is there an X in S satisfying P?" β which has 18 working surface forms in Axioma:
persons: {mike, alice}
# Quantifier family β three spellings, same AST
exists x in persons | x.name == "Mike" # keyword
`exists x in persons | x.name == "Mike" # backtick digraph (ASCII)
β x in persons | x.name == "Mike" # Unicode glyph
# Set-comprehension family β "the matching set is non-empty"
len({x | x <- persons, x.name == "Mike"}) > 0 # Axioma pipe
{x | x <- persons, x.name == "Mike"} != {} # Axioma pipe
not ({x | x <- persons, x.name == "Mike"} == {}) # De Morgan
len({p for p in persons if p.name == "Mike"}) > 0 # Python
{p for p in persons if p.name == "Mike"} != {} # Python
# List-comprehension family β "indicator sum > 0"
sum([1 | p <- persons, p.name == "Mike"]) > 0 # Axioma pipe
sum([1 for p in persons if p.name == "Mike"]) > 0 # Python
len([1 for p in persons if p.name == "Mike"]) > 0 # Python
# Higher-order β reductions over the collection
len(filter(func(p) [p.name == "Mike"], persons)) > 0
reduce(func(acc, p) [acc or p.name == "Mike"], false, persons)
# Walrus binding β compute name once, reuse it
{x | x <- persons, n: x.name, n == "Mike"} != {}
# Lazy + force β produce a generator, then materialize
g: (x | x <- persons, x.name == "Mike")
len(force(g)) > 0
# Lazy + gen_take(1) β SHORT-CIRCUITED existence (asymptotically cheaper)
len(gen_take(1, (x | x <- persons, x.name == "Mike"))) > 0
# Hash destructure β different source shape
hashes: [{name: "Mike"}, {name: "Alice"}]
len({n | {name: n} <- hashes, n == "Mike"}) > 0
# Negated universal β βx.P(x) β‘ Β¬βx.Β¬P(x)
not (forall x in persons | x.name != "Mike")
Plus the related-but-distinct membership test, which presupposes you already have the witness:
mike in persons # "is mike one of them?"
Why so many. Each form aligns with a different
intellectual tradition: math ({x | β¦}), Python
(for x in xs if β¦), SQL
(orderby/having/limit/offset), Haskell (walrus), Prolog
(<- over relational predicates), F-logic (concept
extents, @tag filters), and indicator-sum probability. The
right form is the one your reader will find most natural.
Working showcase: tests/axioma/showcase/method.ax.
Short-circuit note. Quantifier forms
(exists / β) and
gen_take(1, lazy) stop at the first match. Eager
comprehension forms scan the full collection. For large collections the
difference matters β pick the short-circuiting form when the predicate
is cheap and the collection is large.
8. First-Order Logic
Quantifiers
a: {1, 2, 3, 4, 5}
forall x in a: x > 0 # true
forall x in a: x > 3 # false
exists x in a: x > 3 # true
exists x in a: x > 10 # false
Bounded Range & Counting Quantifiers
Axioma supports advanced bounded range quantifiers (which operate over integer intervals without requiring an explicit set domain) and comparison-based counting quantifiers (which assert that a specific number of elements satisfy a predicate):
Bounded Range Quantifiers
- Double-sided range: evaluates the predicate over
the range
[lower..upper](for<=) or[lower..<upper](for<).forall 1 <= x <= 10: x > 0 # true exists 1 < x < 5: x == 3 # true - Single-sided range: defaults to a lower bound of
0and evaluates up to the upper bound.forall x < 5: x >= 0 # true (evaluates x = 0, 1, 2, 3, 4) exists x <= 0: x == 0 # true (evaluates x = 0)
Arbitrary Counting Quantifiers
Checks whether exactly, at least, or at most a specified number of
elements in a set satisfy a predicate. Uses standard comparison
operators (==, >=, <=,
>, <) directly following the
exists keyword:
domain: {1, 2, 3, 4, 5}
exists >= 3 x in domain: x > 2 # true (witnesses: 3, 4, 5)
exists <= 1 x in domain: x > 4 # true (witness: 5)
exists == 2 x in domain: x % 2 == 0 # true (witnesses: 2, 4)
exists > 1 x in domain: x < 3 # true (witnesses: 1, 2)
With predicates
nums: {1, 2, 3, 4, 5, 6}
forall x in nums: positive(x)
exists x in nums: even(x)
forall x in nums: x < 10 and x > 0
Combined
a: {2, 4, 6}
b: {1, 2, 3, 4, 5, 6}
forall x in a: even(x) and (x in b)
exists x in b: odd(x) and (x > 3)
Symbolic-mode quantifiers β textbook syntax
The bounded forms above (forall x in a: ...) iterate a
domain and return a Boolean. Axioma also accepts bare
textbook-style quantifiers with no
in <domain> clause β these produce a
Formula value, a symbolic carrier you can pass to the
resolution/CNF/satisfiability machinery in fol/,
sol/, and logic/.
# Bare textbook form β no marker between variable and body
f1: βx P(x) # β β x. P(x) (type: formula)
f2: βx F(x) # β β x. F(x)
# Dot form
f3: βx. P(x) # identical to f1
f4: βx. F(x)
# ASCII keyword equivalents
forall x P(x)
exists x F(x)
`forall x P(x) # backtick digraph
`exists x F(x)
# Complex bodies β quantifier binds widely
βx P(x) β Q(x) # β β x. (P(x) β Q(x))
βx P(x) β§ Q(x) # β β x. (P(x) β§ Q(x))
βx Β¬P(x) β¨ Q(x) # β β x. ((Β¬P(x)) β¨ Q(x))
# Nested quantifiers
βx βy P(x, y)
βx βy P(x, y) β R(x, y)
Mode summary:
| Quantifier form | Mode | Returns |
|---|---|---|
βx F(x) / βx F(x) |
symbolic | *types.Formula |
βx. F(x) / βx. F(x) |
symbolic | *types.Formula |
βx in S | F(x) |
bounded | Boolean |
β!x in S | F(x) |
bounded uniqueness | Boolean |
β!x F(x) |
symbolic uniqueness | *types.Formula |
Tier 1 limitation: the connectives
Β¬ β§ β¨ β βοΈ on Formula operands still dispatch to Boolean
semantics β building compound formulas like
Β¬βx P(x) βοΈ βx Β¬P(x) at the symbolic level requires Tier 2
Formula-aware connectives. For now, keep connectives inside the
quantifier body.
Other Tier 1 textbook additions
# Biconditional β both Unicode forms lex to IFF
true β false # false (U+2194, modern textbook)
true βΊ false # false (U+27FA, was lexed but never wired)
# Truth constants
β₯ # false (falsum, U+22A5)
β€ # true (verum, U+22A4)
# Proper subset distinct from subset
{1,2} β {1,2,3} # true (proper subset)
{1,2,3} β {1,2,3} # false (equal, not proper)
{1,2} β {1,2} # true (regular subset allows equality)
# Set difference β Enderton/Halmos notation
{1,2,3,4,5} \ {2,4} # {1, 3, 5}
# Uniqueness quantifier
β!x in {1,2,3} | x == 2 # true (exactly one)
Formula type + predicate
f: βx P(x)
@f # "formula"
formula?(f) # true
boolean?(f) # false
See tests/axioma/logic/test_textbook_parity_tier1.ax for full coverage.
9. Multi-Valued Logics
Axioma has five first-class logic kinds, each with
its own truth-value type. Operators (and, or,
not, implies, iff,
xor) automatically dispatch based on operand types. The
dispatch priority is Belnap > Intuit3 > Εukasiewicz >
Kleene > Boolean.
Truth values are first-class. The forms the interpreter prints are themselves source β every non-Boolean truth value has a lexable glyph literal, an ASCII digraph, and a dotted member on its seeded Concept, all byte-identical to the constructor call:
| Logic | Literals | Digraphs | Members |
|---|---|---|---|
| Belnap B4 | β€α΅ β₯α΅ β€β₯α΅ ?α΅ |
`beltrue `belfalse `belboth `belneither (+
`glut / `gap) |
Belnap.both .neither .glut
.gap |
| Kleene K3 | β€α΅ β₯α΅ ?α΅ |
`kltrue `klfalse `klunknown |
Kleene.unknown |
| GΓΆdel G3 | β€β± β₯β± ?β± |
`gtrue `gfalse `gunknown |
Intuit3.unknown |
| Εukasiewicz Ε3 | β€Ε β₯Ε Β½Ε |
`ltrue `lfalse `lhalf |
Lukasiewicz.half |
Bare β€ / β₯ stay Boolean true /
false. Each seeded Concept also carries
values β the logic's canonical domain
(Belnap.values β [β€α΅, β₯α΅, β€β₯α΅, ?α΅]) β and the
literals work as match patterns:
w: β€β₯α΅ # β‘ belnap("both") β byte-identical value
m: match w with | β€β₯α΅ => "glut" | ?α΅ => "gap" | _ => "classical" # β "glut"
forall p in Belnap.values | designated(p or not p) # β false β the gap escapes LEM
Three semantic rules, uniform across the family:
- Truthiness is designation.
if/whileon a typed truth value branch by the logic's designated set β B4{β€α΅, β€β₯α΅}(a glut is true-enough to act on), K3/G3{true}, Ε3{1.0}. Soif β₯α΅ β¦andif ?α΅ β¦take the else-branch, andif xalways agrees withdesignated(x). (Untypedomkeeps its SETL truthiness β see the dual-null note in Β§4.) ==/!=are metalanguage equality β a plain Boolean, coercing the untyped spellings (belnap("true") == "true",?α΅ == om,lukasiewicz(1.0) == 1.0); values of different logics are never equal. The object-language biconditional isiff(?β± iff ?β±ββ€β±). Membership composes too:β€β₯α΅ in Belnap.valuesβtrue.- Operands are validated β no silent coercion. Logic
operators accept a typed value's own kind plus Boolean /
om/ Kleene (the canonical embeddings; Ε3 also takes raw numbers in[0, 1]) and error loudly on anything else:belnap("true") and "banana"is an error, not a quiet gap, and cross-logic mixes without a canonical embedding (β€β₯α΅ and Β½Ε,β€β± and β€β₯α΅) are rejected with a wrap hint.
Boolean
Classical two-valued. Default for
true/false.
true and false # false
true implies false # false
not true # false
Kleene K3 (three-valued)
true / false / unknown. Two
spellings of the same tables: the untyped SETL om /
Ξ© (the historical K3 unknown), and the typed literals
β€α΅ β₯α΅ ?α΅ (β‘ kleene("true") /
kleene("false") / kleene("unknown")).
om and true # Ξ© (unknown β untyped, stays om-flavored)
om or true # true
not om # Ξ©
?α΅ and β€α΅ # ?α΅ (typed β results stay Kleene)
?α΅ == om # true (canonical equality); `if ?α΅` AND `if om` are both
# FALSY now (designation β om folds onto it in 2.5)
Εukasiewicz L3 (real-valued)
Continuous truth values in [0, 1]; the three canonical
points have literals β€Ε (1.0), Β½Ε (0.5),
β₯Ε (0.0).
half: lukasiewicz(0.5) # β‘ Β½Ε
qrtr: lukasiewicz(0.25)
half and qrtr # min(0.5, 0.25) = 0.25
half implies qrtr # min(1, 1 - 0.5 + 0.25) = 0.75
Β½Ε and 0.8 # Β½Ε β raw numbers in [0,1] embed (the fuzzy idiom)
Β½Ε == 0.5 # true; only β€Ε (1.0) is designated/truthy
Belnap B4 (paraconsistent four-valued)
T, F, Both,
Neither β supports contradictory information.
p: β€β₯α΅ # the glut literal β β‘ belnap("both")
q: belnap("true") # constructor form, β‘ β€α΅
p and q # β€β₯α΅ β contamination propagates
Belnap.gap # ?α΅ (Priest's terms: .glut / .gap alias .both / .neither)
truth("parent", "John", "Mary") # Query stored Belnap value (relation named by string)
The four values β€α΅, β₯α΅, β€β₯α΅,
?α΅ are lexable literals (not just display forms);
designated(x) is true for β€α΅ and the glut
β€β₯α΅, and if/while branch
accordingly.
The two orders of B4. B4 is a bilattice:
the bare and/or/not above are the
truth order (how true?), while evidence
combination lives on the knowledge order (how
much information? β neither <
true,false < both), surfaced
as b4_join (β β accept everything every source says) and
b4_meet (β β keep only what all sources agree on):
w1: belnap("true") # witness 1: guilty
w2: belnap("false") # witness 2: innocent
b4_join(w1, w2) # β β€β₯α΅ conflict surfaces as a GLUT (β)
b4_meet(w1, w2) # β ?α΅ no consensus (β)
designated(b4_join(w1, w2)) # β true β a glut is true-enough to act on
w1 and (not w2) # β β€α΅ truth-order `and` can NEVER build a glut
b4_join(w1, w2, om) # variadic; or fold a collection: b4_join([w1, w2])
w1 β w2 # infix glyph forms (β binds tighter than β,
w1 `oplus w2 # mirroring β©/βͺ); ASCII via `oplus / `otimes
Operands must be Belnap/Boolean/om/Kleene (strings error
β wrap with belnap(...)). The same strictness covers the
truth-order operators
(and/or/not/implies/iff/xor)
since July 2026 β see rule 3 above.
Evidence can also accumulate on a stored fact:
set_truth_combine(rel, args..., value) β-merges the
incoming value onto the fact's stored B4 truth instead of overwriting it
(contrast set_truth, which is last-write-wins):
relation rain(city)
axiom rain("seattle") # truth defaults to β€α΅
set_truth_combine("rain", "seattle", "false") # β€α΅ β β₯α΅ β stores + returns β€β₯α΅
truth("rain", "seattle") # β β€β₯α΅ β the conflict is KEPT
The fact must already be stored, and the strict-bivalence mode
(set_truth_logic("Boolean")) rejects a glut-producing
report (non-fatal false, stored truth unchanged).
GΓΆdel G3 (intuitionistic three-valued)
true / false / unknown
(literals β€β± β₯β± ?β±) β but with
intuitionistic semantics:
p: ?β± # β‘ intuit3("unknown")
not p # β₯β± (G3 collapses U to F; K3 keeps U)
p implies p # β€β± (reflexive β always)
g3_lem(?β±) # ?β± β LEM is NOT a tautology
g3_dne(?β±) # ?β± β double-negation elimination FAILS
designated(g3_lem(?β±)) # false β so LEM fails as an inference too
p == β€β± # false β plain Boolean METALANGUAGE equality
p iff p # β€β± β the object-language biconditional
The g3_* helpers (g3_and /
g3_or / g3_not / g3_implies /
g3_lem / g3_dne) return typed Intuit3
values, so their results flow straight back into
and / not / designated.
(== on G3 values used to return the biconditional itself β
a truthy ?β± for unknown == true; it is now
Boolean equality, and the biconditional lives at iff.)
Constructors, types, and equality
The literals cover the canonical values; the
constructors are the dynamic/coercion form β
belnap() (B4), lukasiewicz() (Ε3, any value in
[0,1]), intuit3() (G3), and
kleene() (K3) β for converting a runtime
String/Boolean/om into a truth value. type(x)
and the @x type-of sigil return the proper TitleCase name,
and each value is is-checkable against its seeded primitive
Concept:
ku: kleene("unknown") # also kleene(om), kleene("u"), kleene("?") β or just ?α΅
type(ku) # β "Kleene" (@ku is the same)
ku is Kleene # β true
ku == om # β true (coerces to its canonical Boolean/Om form)
kt: kleene("true")
kt and ku # β ?α΅ (results stay Kleene)
not ku # β ?α΅
ku implies ku # β ?α΅ (K3 β contrast G3's reflexive β β€β±)
type(β€β₯α΅) # β "Belnap"
type(Β½Ε) # β "Lukasiewicz"
type(?β±) # β "Intuit3"
# Equality coerces each value's own untyped spellings β and ONLY those:
belnap("true") == "true" # β true
β€β₯α΅ == "both" # β true
lukasiewicz(1.0) == 1.0 # β true (== 2 is false β no clamping)
β€α΅ == β€α΅ # β false (cross-LOGIC values are never ==)
Note: Kleene's
unknownis still represented byom(Ξ©) at the operator level β the existingom and truetables are unchanged.kleene(...)/?α΅is the typed form: it normalizes toomfor evaluation, runs the same K3 tables, then re-wraps logic results back into aKleeneso they stay introspectable (mirroring howbelnap/lukasiewicz/intuit3operators return their own type). No asymmetry any more:?α΅ == omis true, and bothif ?α΅andif omare falsy (designation βomfolds onto the designation path in step 2.5, matching the typed unknown it equals).
Truth tables
Kleene show truth_table for and
Lukasiewicz show truth_table for implies
Belnap show truth_table for or
Intuit3 show truth_table for implies
tableform(f, "belnap") prints the full grid of any
function over an MVL domain ("kleene" /
"lukasiewicz" / "intuit3" /
"boolean" too) β and since the cells are lexable literals,
a printed cell pastes back into the REPL as the value it shows.
10. Modal, Temporal, Epistemic & Deontic Logic
Modal operators
necessarily(p) # β‘p β p holds in all accessible worlds
possibly(p) # βp β p holds in some accessible world
Temporal logic
always(p) # G p β p holds at all future times
eventually(p) # F p β p holds at some future time
next(p) # X p β p holds at the next time step
until(p, q) # p U q β p until q
Epistemic logic
knows("alice", p) # K_alice p β Alice knows p
believes("bob", p) # B_bob p
common_knowledge(p, agents) # CK p among agents
Deontic logic
obligatory(action)
permitted(action)
forbidden(action)
The examples above are the public modal/temporal/epistemic/deontic surface in this manual; implementation notes live with the corresponding evaluator code.
11. Fuzzy Logic & Higher-Order Logic (SOL)
Fuzzy logic
Continuous truth in [0, 1] with fuzzy set
membership:
tall: fuzzy_set("tall", lambda h => sigmoid(h - 180))
membership(tall, 175) # 0.38
membership(tall, 190) # 0.88
Second-Order Logic (SOL)
Quantification over predicates and functions:
forall_pred P: forall x: P(x) implies P(x) # Trivially true
exists_pred P: forall x in domain: P(x) # βP. βx. P(x)
Treat the SOL examples here as the public surface; the implementation is experimental and may change as the quantified logic engines mature.
12. Lambda Calculus & Higher-Order Functions
Function definitions
inc: lambda x => x + 1 # lambda arrow (multi-arg needs parens)
add: lambda (x, y) => x + y
multiply: func(a, b, c) [a * b * c] # canonical REBOL bind
func double(x) [x * 2] # named declaration (optional keyword form)
double(x) = 2 * x # EQUATION form β same function, no keyword
Equations β
name(args) = expr
The keyword-free equation is the textbook spelling
of a function definition β f(x) = 2 * x reads exactly as
mathematics writes it (and as Julia's short form). It is pure sugar: an
all-plain-parameter equation is byte-identical to
func name(args) [expr] (so it compiles under
--vm and redefinition is last-write-wins), and an equation
with patterns or a when
guard is byte-identical to a clausal func (clauses
accumulate by name in source order; unmatched calls fall through to
none; --typecheck exhaustiveness applies). The
two spellings may be mixed in one clause group.
double(x) = 2 * x # β‘ func double(x) [x * 2]
fact(0) = 1 # literal-pattern clause β¦
fact(n) = n * fact(n - 1) # β¦ + catch-all: fact(5) β 120
sign(x) when x > 0 = 1 # guard sits between the head and `=`
sign(x) = 0 # (Haskell's own guard order, spelled `when`)
head2([h | t]) = h # cons patterns work; so do constructor,
area(Circle(r)) = 3.14 * r * r # tuple, wildcard, or- and as-patterns
A bracket after the head is the BODY, exactly as in
the func spelling β the two are one construct with one body
parser, so they cannot mean different things. An array is the
double bracket, its inner bracket sitting in value
position as the body's final expression:
boxed(x) = [2 * x] # BODY β 2 * x β‘ func boxed(x) [2 * x]
arrayed(x) = [[2 * x]] # the 1-element ARRAY β [2 * x]
norm(x) = [ # several statements β the last is the value
m: x * 2
m + 1
]
pairup(x) = [x, x + 1] # commas in a body β the tuple (x, x + 1)
plain(x) = 2 * x # no brackets needed for a one-expression body
literal(x) = ([1, 2, 3])[x] # a bracket wanted as a VALUE is parenthesized
A bare = binding is unaffected β
v = [2 * x] is still the array, because it is a value
binding, not a definition. The difference is the parenthesized head:
f(x) = β¦ defines a function and takes a body,
v = β¦ binds a value.
Boundaries: the head's parameters are patterns
(typed/default/named parameters stay on the func spelling);
a capitalized parameter is a nullary constructor
pattern, not a binder β the same rule
func f(X) [β¦] and match follow;
answer = 42 (no parens) remains an ordinary value binding;
and the bare Haskell spelling double x = 2 * x stays a
SyntaxError whose hint names this form β Axioma calls are parenthesized,
so definitions are too. This completes the keyword-optional lattice:
relations may drop relation on the bare uppercase header
(pair(X, Y)), rules may drop rule before
:-/whenever, and functions may drop
func before =.
Return values β
the last expression, and return
A function body returns its last evaluated
expression β that is the native idiom, and most Axioma
functions never write return at all. For early exits,
return [value] is a statement (bare
return yields none), and it unwinds through
nested blocks and loops to the enclosing function:
func round_pil(x) [
f: floor(x)
if x == f then return f # branch-position return (guard clause)
else return floor(x + 0.5)
]
func classify_sign(x) [
return "neg" if x < 0 # postfix guards β the Perl idiom
return "zero" unless x != 0
"pos" # fall-through: last expression
]
func find_first_even(xs) [
foreach x in xs [
if x == 0 then return x # unwinds through the loop
]
none
]
if c then return v is sugar for the block form
if c then [return v] β byte-identical AST. Because
return is a statement, it cannot appear in value
position: x: return 5 is a SyntaxError (with a hint) β bind
the value directly. A return at top level (outside any
function) is a benign no-op; the return value must
start on the same line as the keyword (a line break after
return means a bare return). Under --vm, every
function-level form compiles (the frame machinery already existed);
top-level return stays evaluator-only.
Several results are one tuple. A function returns
exactly one value, and Tuple is the product type β so write
return (best, at) and destructure at the call site with a
multi-assign: m, i = maximum(xs). The comma is a
separator everywhere in Axioma (arguments, literals,
comprehension clauses) β never an operator that builds values β so the
bare spelling return best, at is a SyntaxError whose hint
names the exact rewrite:
SyntaxError: 'return' takes one value
Hint: a comma list is not a value β to return several results, return one
tuple: `return (best, at)`. The call site destructures it: `a, b = f(...)`.
Contracts β
requires / ensures / check f
A function's spec is data: the contract
statement declares it next to the function, every later call enforces
it, and check f verifies it over generated inputs. This is
the function twin of the concept formation layer
(concept X { purpose:, examples:, boundary: } +
check X β Β§ Concept formation), and the formal home of the
accumulator-seed bug from the pitfall box:
types cannot reject a well-typed wrong answer, but a
postcondition can.
maximum: func(a) [
best, at: a[1], 1
for i, e in enumerate(a) [
if e > best then [
best = e
at = i
]
]
return (best, at)
]
contract maximum {
purpose: "Largest element of a non-empty array, with its 1-based position."
requires: len(a) > 0
ensures: result[1] in a
ensures: forall e in a | result[1] >= e
generate: func() [ map(func(k) [random(-99, 99)], range(1, random(1, 8))) ]
trials: 200
}
maximum([8, 10, 23, 12, 5]) # (23, 3) β passing calls are unchanged
maximum([]) # ERROR: contract violated: maximum requires
# clause 1 (len(a) > 0) + an args: line
v: check maximum # runs the trials β β€α΅ (a Belnap verdict)
contract is a soft keyword β
contract: value bindings, hash keys, and argument uses all
keep working; the statement fires only in the exact shape
contract name { β¦ }. The braces are the declarative
slot block (the concept surface), not an executable
[ β¦ ] body β a contract is declared metadata. Slots, all
optional; requires:/ensures: repeat and
conjoin:
| Slot | Meaning |
|---|---|
purpose: |
Prose. Shown by describe(f) (Purpose:
line). |
requires: |
Precondition over the parameter names. Checked
before the body; a false clause is a catchable
contract violated: Error naming the clause, with an
args: line. |
ensures: |
Postcondition over the parameters plus
result (the return value β bound only inside the
clause; outer result bindings are untouched). Checked
after the body. |
generate: |
A zero-arg function returning one argument tuple per call (a Tuple splats to multiple arguments; any other value is the single argument) β or a ready-made Array/Set of argument tuples, used as-is. |
examples: |
Array/Set of argument tuples check always runs first
(deterministic anchors). |
classify: |
A function over the same arguments returning a String label;
check prints the label distribution under
CHECK PASS (see below). |
trials: |
How many generated draws check makes (default
100). |
Enforcement is always on for a contracted function β
the cost is opt-in by declaring, and it survives rebinding,
map/filter, and partial application (a partial
defers checking to the eventual saturated call). Re-declaring a contract
replaces it. Clauses evaluate in a child of the function's closure env,
so they may call outer helpers. The contract attaches to the
function object, so contract f must come
after f is defined; multi-clause functions
(func fib(0) [0] β¦) take one contract on the shared name,
with pattern variables bound positionally
(fib(0)/fib(n) β the contract's n
sees the argument).
when guards vs requires. A
guard selects among clauses β no clause matching is a
legitimate outcome (om, the total-match rule).
A requires clause rejects β calling outside the
precondition is a loud, catchable Error. Both compose: guards route,
requires polices.
check f β property verification. Runs
the examples: first, then trials: draws from
generate:; every input that passes requires:
is applied and every ensures: clause evaluated. The verdict
is a Belnap value, mirroring check Concept:
β€α΅β every input passed (aβ --- CHECK PASS:summary prints);β₯α΅β a counterexample or a call error was found; the first one stops the run and prints a report with theargs:,result:, and how many inputs preceded it;?α΅β nothing checkable: no contract, no input source, or every input was skipped byrequires:(a generated input failing the precondition is discarded and counted, never a failure).
Generation runs under the ambient seeded RNG (fixed
startup seed β Β§ Randomness), so an unseeded check is
reproducible run-to-run; call random_seed(β¦) first to vary
the draws. Note the target parses like any check
expression, so check f == x reads as
check (f == x) β bind the verdict first
(v: check f) to compare it.
Shrinking. A failing witness is minimized
before it is reported: check greedily simplifies
the counterexample β integers move toward 0, arrays and
strings lose halves and single elements, one argument position at a time
β accepting a simpler candidate whenever it still fails (any
clause; the input must also still satisfy requires:, so
shrinking never leaves the declared domain). The report's
args: line shows the minimal witness and a
shrunk from: (β¦) in N steps line shows the original
draw:
β --- CHECK FAIL: bad_maximum ensures clause 1 (result[1] in a)
args: ([-1])
shrunk from: ([-31, -74, -63, -10, -49]) in 6 steps
result: (0, 0)
Shrinking is deterministic (no randomness) and capped at 500 candidate evaluations; when nothing improves, the line is omitted.
Input distribution β classify:. A
passing suite can pass vacuously β the seed-bug lesson was that
all-positive test data never exercises the interesting region. The
classify: labeler makes the distribution visible: every
input that runs is labeled, and CHECK PASS prints a census
(percentages, largest first):
β
--- CHECK PASS: maximum (0 examples + 200 trials)
67% mixed Β· 19% all-negative Β· 15% non-negative
Auto-generation from :: annotations.
When a contract declares no generate: and
every parameter carries a supported scalar annotation β
Integer, Float, String, or
Boolean β check synthesizes the draws itself
(integers and floats in Β±100, strings of up to 8 lowercase letters, a
fair coin), through the same seeded source as random. The
summary says auto trials so the mode is visible:
inc: func(x :: Integer) [x + 1]
contract inc { ensures: result == x + 1 }
v: check inc # β
--- CHECK PASS: inc (0 examples + 100 auto trials)
Containers are deliberately not auto-drawn (annotations carry no
element type) β write a generate: for structured inputs. A
partially annotated function keeps the ?α΅ verdict, with the
message pointing at both options.
Pre-state β old(). Inside an
ensures: clause, old(e) denotes the value
e had on entry to the call, so a
postcondition can relate the result (or a mutated argument) to the
pre-call state:
append_x: func(xs) [ push(xs, "x") ]
contract append_x { ensures: len(result) == old(len(xs)) + 1 }
append_x([1, 2]) # 3 elements β the clause compares against the ENTRY length
The capture is by value: every old(...)
argument is evaluated before the body runs, and composite results
(arrays, dictionaries, sets, tuples) are deep-copied, so an in-place
mutation by the body cannot retro-edit the snapshot (Eiffel's
old captures references and traps users exactly there;
prefer scalar captures like old(len(xs)) when the whole
collection isn't needed). old() takes exactly one
expression and cannot nest; it is meaningful only in
ensures: β in requires: the pre-state
is the arguments. A binding named old in scope
wins: the operator stands down and the call is ordinary code.
Termination β decreases:. The slot
declares a termination measure over the parameters β an Integer that
must be β₯ 0 and must strictly decrease
on every recursive activation. The check is dynamic (the static
discharge belongs to a future prove f), so it reports on
the calls that actually happen β including self-tail-call iterations,
which the tail-call optimizer runs in constant space where no recursion
limit would ever trip:
fact: func(n) [ if n <= 1 then 1 else n * fact(n - 1) ]
contract fact { requires: n >= 0, decreases: n }
fact(5) # 120 β measure 5 β 4 β 3 β 2 β 1
stuck: func(n) [ if n == 0 then 0 else stuck(n) ]
contract stuck { decreases: n }
try(stuck(3)) # catchable: β¦ measure did not decrease (3 β 3)
# (without the contract this tail loop spins forever)
The measure may be any Integer expression over the parameters
(decreases: hi - lo for a shrinking span). A negative
measure and a non-Integer measure are loud catchable errors. Under
check f the measure stays live even while
requires/ensures enforcement is suspended for the trials β so a
generator that draws a non-terminating input turns the hang into a
failed trial (β₯α΅ with a shrunk witness). Mutual recursion
is checked per function (each function's own measure across its own
activations).
The concept twin β invariant:. Concepts
carry the instance-level counterpart: one
invariant: <predicate> slot per concept
block, open in slot names like boundary:, checked
after instantiation and after every direct property
write (dot assignment, compound assignment, and the
x[attr -> val] frame form):
concept Account {
balance: 0
invariant: balance >= 0
}
acc: an Account { balance: 10 } # checked at creation
acc.balance = 5 # checked after the write β fine
poke: func() [ acc.balance = -1 ]
try(poke()) # catchable: concept invariant violated: Account (balance >= 0)
acc.balance # 5 β the violating write REVERTED
try(an Account { balance: -3 }) # stillborn: no instance created, extent unchanged
A violating write is rolled back before the error returns, so the
object never stays inconsistent (deliberately stronger than Eiffel's
exception-leaves-object-dirty), and a violating creation is stillborn.
Invariants are strict (invariant~: is rejected β spell
conjunctions with and), inherit down extends
chains (a child instance must satisfy its ancestors' invariants), and
are error-loud: a predicate that fails to evaluate or returns a
non-Boolean is itself a catchable error. Indirect writes β inverse-slot
propagation, unify coreference merges, and
Concept has default edits β are not invariant-checked in
this round.
Proof instead of sampling β prove f.
check f samples inputs; prove f
decides all of them. It hands the verification condition
(requires β§ result = body) implies ensures β one ensures
clause at a time β to the SMT solver, so a verdict covers every input at
once:
inc: func(x) [ x + 1 ]
contract inc { requires: x > 0, ensures: result > x }
prove inc # β
PROVE OK: inc (1 ensures clause discharged for ALL inputs)
bad: func(x) [ x + 1 ]
contract bad { ensures: result > 10 }
prove bad # β PROVE FAIL β counterexample: result = 10, x = 9
A refutation is a proof of failure, not an unlucky draw: the
solver exhibits the input rather than stumbling on it. Inside
ensures, old(e) is rewritten to e
β in the verification condition the parameters already denote their
entry values β so pre-state contracts prove without extra machinery.
Branching bodies are in scope (if/else becomes
a conditional term, its two arms sort-unified).
The verdict is Belnap, and the third value carries the weight: β€α΅
discharged, β₯α΅ refuted, ?α΅ not discharged.
Not-discharged is never reported as refuted. Calls to helpers and
builtins are uninterpreted in the solver β it may read
len however it likes β so a counterexample involving one is
an artifact of a missing definition, not evidence your contract is
wrong:
push_end: func(xs) [ push(xs, 1) ]
contract push_end { ensures: len(result) == old(len(xs)) + 1 }
prove push_end # ?α΅ β `len`, `push` have no definition in the solver
# (use `check push_end`, which runs the real ones)
Multi-clause functions, indexing, bounded
forall e in a | and nonlinear * are refused by
name with a pointer at check. prove f is both
a statement and an expression, so v: prove f binds the
verdict exactly as v: check f does.
Functions as arguments β sub-contracts and blame. A
function-valued parameter can carry its own contract in a nested block.
Its clauses speak positionally β arg (or
arg1β¦argN, and args) and
result β because the contract's author does not choose the
lambda the caller passes:
twice: func(f, x) [ f(f(x)) ]
contract twice {
f: { requires: arg >= 0, ensures: result >= arg }
ensures: result >= x
}
twice(func(n) [ n + 1 ], 5) # 7
When one of these fails, the report names who is at
fault, and the two directions are opposite. A sub-contract's
requires failing means twice called the
supplied function outside that function's domain β the callee
is to blame. Its ensures failing means the function the
caller supplied broke its own promise:
contract violated: twice's f ensures clause 1 (result >= arg)
args: (5)
result: -95
blame: the caller of twice β the function it supplied for f
broke that function's own promise
The incoming function is wrapped so the guard travels with the value:
it still holds when the body passes it to map. Two
consequences worth knowing: a wrapped function is a distinct object
(identity comparison does not survive wrapping), and a recursive
supplied function is guarded at its outermost call only, since its
by-name self-calls reach the original.
Objects over time β stateful checking. A contract on
a concept describes behavior across a lifetime rather than one
call. check then runs random sequences of the listed
operations against fresh instances, with the concept's
invariant: as the oracle:
concept Account { balance: 0, invariant: balance >= 0 }
deposit: func(acc, n :: Integer) [ acc.balance = acc.balance + abs(n) ]
withdraw: func(acc, n :: Integer) [ acc.balance = acc.balance - abs(n) ]
contract Account { operations: [deposit, withdraw], steps: 12, trials: 40 }
check Account
# β --- CHECK FAIL: Account (stateful)
# sequence (1 operation, shrunk from 2):
# β withdraw(1)
# concept invariant violated: Account (balance >= 0)
The deliverable is the sequence, shrunk to the
fewest operations that still break the object β an unshrunk twelve-step
transcript is unreadable. Arguments after the receiver are drawn from
the operations' :: annotations, so the same seeding and
sizing rules apply. Each trial's instance is released afterwards,
leaving the concept's extent exactly as it was.
check Concept keeps its formation meaning (over
examples: / counterexamples:) for every
concept that declares no stateful contract β the stateful reading is
chosen by declaring one, never inferred.
Sized generation. Generated inputs start small and
grow across the run, so shallow bugs surface in the first trials instead
of arriving as a large witness that then has to be shrunk. A
generate: function opts in by taking one parameter:
contract sample { generate: func(size) [ random(0 - size, size) ], trials: 5 }
# sizes drawn: 1, 25, 50, 75, 100
A zero-argument generator is called exactly as before, so existing
contracts draw identically. Auto-generation from ::
annotations is sized too β magnitudes and string lengths follow the
ramp, while Booleans do not (a coin has no magnitude). The size is a
function of the trial index alone, never a draw from the random source,
so seeded runs stay reproducible.
The seed-bug version of maximum (seeding
best with 0) fails the membership clause on
any all-negative input β maximum([-4, -2, -9]) errors at
the call with result: (0, 0) in the report, and
check maximum finds it in the first few trials.
Evaluator-only: the contract statement and
check f reject under --vm, and contracted
functions called under --vm run without
enforcement (the VM never consults the contract). Reference:
doc contract, doc check,
doc prove; tests: tests/axioma/contracts/ and tests/axioma/concepts/ for the
concept invariant and stateful-checking files.
Anonymous functions
(lambda x => x * 2)(5) # 10
Closures
makeAdder: lambda n => lambda x => x + n
addTen: makeAdder(10)
addTen(5) # 15
Closures also mutate captured state, with
rebind β a plain assignment stops at the closure's own
frame and would declare a local there instead. The counter idiom and the
full scoping rules live in Β§3 Scoping
& shadowing.
make_counter: func() [
acc: 0
func() [rebind acc: acc + 1
acc]
]
c: make_counter()
c() ; c() # 2
Currying & partial application
multiply: lambda x => lambda y => x * y
double: multiply(2)
triple: multiply(3)
Function composition
Composition wires two functions end to end: the output of one becomes the input of the next, and the pair behaves as a single new function. Because the result is itself a function, compositions nest without limit β that closure property is what makes point-free style possible.
Axioma ships both directions, deliberately distinct, because mathematics and pipelines read opposite ways and neither audience can be quietly overruled.
f: func(x) [x + 1]
g: func(y) [y * 10]
# ββ left-to-right (pipeline order): f runs FIRST ββ
compose(f, g)(3) # β 40 the named form, n-ary
(f >> g)(3) # β 40 the infix form
# ββ right-to-left (mathematical order): f still runs first ββ
(g β f)(3) # β 40 the glyph
(g << f)(3) # β 40 its ASCII twin
(g `circ f)(3) # β 40 the backtick digraph (`ring` also works)
The bridging identity, which holds in every spelling:
f >> g == compose(f, g) == g << f == g β f
Why two directions. On functions, f β g
universally means "apply g first" β a language that
reversed the glyph would be wrong to every reader who learned it from a
textbook. compose cannot follow the glyph, because it
also composes relations, and a function is its graph:
compose(R, S) is
{(x,z) | (x,y) β R β§ (y,z) β S}, left to right, which fixes
compose(f, g) as x β¦ g(f(x)). Both readings
are forced; neither is a preference. They are mirrors, not aliases.
Precedence. CALL binds tighter than
every composition operator, so f >> g(3) reads
f >> (g(3)). Write (f >> g)(3) to
apply the composition β exactly as the parenthesisation works on
paper.
Composed functions are values
normalize: trim >> lower # point-free: the argument is never named
map(normalize, [" A ", "B "]) # β ["a", "b"]
pipeline: compose(f, g)
map(pipeline, [1, 2, 3]) # β [20, 30, 40]
{p: compose(f, g)}.p(1) # β 20 stored in a dict
twice: func(k) [compose(k, k)] # returned from a function
The first stage receives every argument the composition was called with, so a pipeline may open from a multi-argument function; each later stage receives the single value its predecessor produced.
add: func(a, b) [a + b]
compose(add, g)(3, 4) # β 70 add sees both arguments
The degenerate arities
Composition has an identity element, so the empty and single cases are that element and that function β not errors. This is what lets a stage list be folded at any length, including zero.
compose()(7) # β 7 the identity element
compose(f)(3) # β 4 one stage is that stage
reduce(func(acc, s) [compose(acc, s)], compose(), [f, g])(3) # β 40
Relations compose too
compose does double duty. On sets of pairs it is
relational composition:
compose({("a",1), ("b",2)}, {(1,"x"), (2,"y")}) # β {("a","x"), ("b","y")}
The operators refuse sets on purpose. Relational
R β S has two incompatible conventions in the literature,
so a glyph there would have to guess; compose is the
spelling that commits to one in its name, and the error says so. Mixing
a set with a function is a category error, not a coercion.
The surrounding combinators
| Form | Meaning | Example |
|---|---|---|
identity(x) |
the identity element | identity(7) β 7 |
pipe(x, f, g) |
thread a value through stages | pipe(3, f, g) β 40 |
x |> f |> g |
the operator form of pipe |
3 |> f |> g β 40 |
partial(f, a) |
fix leading arguments | partial(sub, 10)(3) β 7 |
curry(f) |
one argument at a time | curry(sub)(10)(3) β 7 |
flip(f) |
swap the first two arguments | flip(sub)(2, 10) β 8 |
iterate(f, x, n) |
repeated self-composition | iterate(f, 0, 4) β [0,1,2,3] |
converge(c, [f, g]) |
fork: one input, several functions | see below |
on(cmp, key) |
preprocess both arguments | see below |
flip(f) returns the swapped function (so it composes);
flip(f, a, b) applies the swap immediately.
converge β
branching and reconverging
Composition builds a chain. Real point-free code constantly needs a branch that reconverges β a mean is the sum and the length of the same input, divided. Without this the style collapses back to naming the argument just to mention it twice.
total: func(xs) [reduce(func(a, b) [a + b], 0, xs)]
count_of: func(xs) [len(xs)]
div: func(a, b) [a / b]
mean: converge(div, [total, count_of])
mean([2, 4, 6, 8]) # β 5
Every branch receives the full argument list, so a fork can open from
a multi-argument function. The branch list is an Array so
the combiner stays in first position, matching
map/filter/reduce.
on β the
shape composition cannot express
cmp β key would feed key's single result to
a two-argument function. on applies the preprocessor to
both arguments instead:
on(cmp, key)(a, b) == cmp(key(a), key(b))
longer: on(func(a, b) [a > b], func(s) [len(s)])
longer("abcd", "ab") # β true
sort_by(key, coll) covers the common case for one verb;
on generalises the key extraction to any binary
function.
|>is not composition. It threads a value, so3 |> f |> gis a one-shot computation with a3baked into it.f |> gcomputesg(f)β a type error, not a pipeline. Usecompose(f, g)orf >> gwhen you want a function you can keep.
VM: the composition operators and the returning
combinators (compose, pipe,
curry, partial, flip,
converge, on) are evaluator-only pending the
VM parity effort. Each rejects under --vm
rather than returning a wrong answer.
A full treatment β the history from group theory through the lambda calculus to modern functional languages, and how Axioma's surface compares β is in Function Composition in Axioma.
Map / Filter / Reduce
nums: {1, 2, 3, 4, 5}
map(lambda x => x * x, nums) # {1, 4, 9, 16, 25}
filter(lambda x => x > 2, nums) # {3, 4, 5}
reduce(lambda (acc, x) => acc + x, 0, nums) # 15
sum(nums) # 15 (works on arrays/sets/tuples)
Ξ£(nums) # 15 (Unicode alias for sum)
Enumerable verbs
The wider collection vocabulary, beyond
map/filter/reduce/group_by.
Every verb is collection-last, so it threads through
the forward pipe |> with no _ hole
(people |> max_by(...),
xs |> each_slice(3)).
tally(["a", "b", "a"]) # β {a: 2, b: 1} count occurrences (alias: frequencies)
words: ["hi", "hello", "hey", "yo"]
max_by(func(w) [len(w)], words) # β "hello" element with the largest key (argmax)
min_by(func(w) [len(w)], words) # β "hi" argmin; first-wins on ties; none if empty
sort_by(func(w) [len(w)], words) # β ["hi", "yo", "hey", "hello"] ordered by a derived key
sort_by(func(x) [x], [10, 2, 33, 4]) # β [2, 4, 10, 33] numeric-aware (not lexicographic), stable
flat_map(func(r) [r], [[1, 2], [3]]) # β [1, 2, 3] map then flatten one level
take_while(func(x) [x < 5], [1, 2, 9, 1]) # β [1, 2] leading run while the predicate holds
drop_while(func(x) [x < 5], [1, 2, 9, 1]) # β [9, 1] the complement
each_slice(3, [1, 2, 3, 4, 5, 6, 7]) # β [[1,2,3], [4,5,6], [7]] consecutive fixed chunks
each_cons(2, [1, 2, 3, 4]) # β [[1,2], [2,3], [3,4]] sliding windows
chunk([1, 1, 2, 3, 3]) # β [[1,1], [2], [3,3]] group consecutive-equal runs
detect(func(x) [x > 3], [1, 2, 3, 4]) # β 4 first element matching (none if no match)
compact([1, none, 2, none, 3]) # β [1, 2, 3] drop none (keeps om and everything else)
[3, 1, 2] |> sort |> tap(func(a) [println(a)]) # peek mid-pipeline, return the value unchanged
# the payoff β a readable left-to-right pipeline:
[5, 3, 8, 1] |> sort_by(func(x) [x]) |> take_while(func(x) [x < 6]) # β [1, 3, 5]
min_by/max_by return the element
whose key is extremal (not the key value). sort_by shares
sort's numeric-aware ordering, so integer keys sort by
value. tally/frequencies return a dictionary
(a string is counted character by character). Ruby's first-match is
find/detect; find is a reserved
solver keyword here, so the collection verb is detect.
tap runs its function for a side effect and returns the
value unchanged β handy for peeking inside a |> chain.
All work under --vm.
List-library verbs (Haskell / OCaml)
The rest of the classic functional list library. Same
function-first, collection-last shape as the Enumerable
verbs, so they chain through |>.
zip_with(func(a, b) [a + b], [1, 2, 3], [10, 20]) # β [11, 22] pairwise, truncates to shorter
scanl(func(a, x) [a + x], 0, [1, 2, 3]) # β [0, 1, 3, 6] left fold, keeping every step
scanr(func(x, a) [x + a], 0, [1, 2, 3]) # β [6, 5, 3, 0] right fold (f receives (x, acc))
iterate(func(v) [v * 2], 1, 5) # β [1, 2, 4, 8, 16] first n of [x, f(x), f(f(x)), β¦]
iterate(func(v) [v * 2], 1) # β <generator> the same sequence, unbounded and lazy
span(func(x) [x < 3], [1, 2, 3, 4, 1]) # β ([1, 2], [3, 4, 1]) (take_while, drop_while), one pass
separate(func(x) [x > 2], [1, 2, 3, 4]) # β ([3, 4], [1, 2]) partition by predicate
all?(func(x) [x > 0], [1, 2, 3]) # β true holds for every element (true on empty)
any?(func(x) [x > 2], [1, 2, 3]) # β true holds for some element (false on empty)
none?(func(x) [x > 5], [1, 2, 3]) # β true holds for no element (true on empty)
# unfold β the anamorphism, dual of reduce. The step returns Some((value,
# next_seed)) to continue or None to stop, using the built-in Option:
unfold(func(n) [if n > 5 then None else Some((n, n + 1))], 1) # β [1, 2, 3, 4, 5]
scanl/scanr are the folds
reduce/foldr with every intermediate kept
(length n+1: scanl starts with the seed, scanr
ends with it). span is exactly
(take_while(p, xs), drop_while(p, xs)) computed in one
pass; separate is partition under a different
name (partition is reserved for concept partitions).
all?/any?/none? short-circuit and
read correctly in if.
The nine verbs above unfold work under --vm
(byte-identical). unfold is
evaluator-only: its step function builds
Some/None, which are ADT constructors the VM
does not compile, so an unfold under --vm is
rejected at compile time rather than run.
13. The Concept System
A concept is Axioma's central unit of knowledge representation β far more than an object-oriented class or record. Axioma's types are concepts, but a concept is at once:
- a type β the built-in primitives
(
Integer,Float,Set,Stack, β¦) are themselves concepts, so5 is Integerandx :: Dayrun the same machinery as any concept you declare; - a frame β a named thing whose
slots carry values and metadata (inverse,
transitive, cumulative, cardinality), with an auto-maintained
extent of its instances you can iterate like a set
(
{x | x <- Country}), and a conceptβοΈrelationβοΈrule duality; - a description-logic concept β composable with
β β Β¬, ordered by subsumptionβ/β‘, with role restrictions (βR.C,βR.C),Thing/Nothing(β€ / β₯), defined concepts, and partitions β all decided by a real ALC tableau reasoner (satisfiable, subsumption); - a classifier β the
iscopula tests Russell's predication (β,rex is Dog) and class inclusion (β,Dog is Animal); adefinespredicate or aboundaryturns membership into a rule, and every classification carries an epistemic grounding and may be defeasible; - a designed, inspectable idea β the
concept-formation layer (
purpose,examples/counterexamples,formed_by,default_grounding) lets you state why a concept exists andcheckthat it does its job β and aninvariant:slot guards every instance's integrity at creation and on every property write (see The concept twin βinvariant:under Β§Functions Contracts).
The surface is natural language throughout β
concept Stock, Stock has price,
rex: a Dog, rex is Dog,
Dog extends Animal. The rest of this section works through
each of these dimensions; together they make concepts the substrate of
Axioma's knowledge representation and its cognitive paradigm.
Defining concepts
Axioma has two canonical concept-declaration surfaces:
There is a single canonical creation surface β the
keyword-first concept X form. It absorbs four feature slots
that were previously distributed across multiple competing forms:
- bare:
concept X - with postfix doc string:
concept X "doc" - with prefix refinement:
concept/persist X,concept/transient X,concept/system X - with refinement + doc:
concept/persist X "doc" - with slot-defaults block:
concept X { slot: default, ... } - with refinement + block:
concept/persist X { ... } - namespaced:
concept FinKB.Stock,concept/persist FinKB.Bond "doc"
concept Stock # bare concept creation
concept Stock "A financial instrument" # with postfix doc
concept Stock { # with initial slot defaults
price: 0
ticker: ""
}
concept/persist Stock # force KB persistence
concept/transient Scratch # never persist
concept/system InternalRegistry # mark as system-internal
concept/persist Stock "force-persist regardless of mode"
Stock extends Asset # specialize Stock as a subclass of Asset
apple is Stock # classify apple as a Stock (predication)
The keyword-first form pairs naturally with the rest of Axioma's
speech-act family (define, axiom,
postulate). is remains the canonical operator
for instance classification (apple is Stock) and Boolean
type queries (x is Stock in expression position).
Doc strings are also accepted inside the block form:
concept TreasurySec { doc: "A marketable US Treasury security" }
concept TreasurySec "A marketable US Treasury security"
TBill extends TreasurySec "Treasury bill, max 365 days"
TreasurySec has issuer: "US Treasury"
TBill has max_maturity_days: 365
Name-inference form β an anonymous
concept { ... } literal on the RHS of a :
binding picks up its name from the LHS identifier. The resulting Concept
is fully named and behaves identically to
concept X { ... }:
Widget: concept { price: 0, ticker: "" } # name inferred from LHS
Gadget: concept { color: "red", count: 5 }
Foo: concept FooBar { rate: 0.05 } # explicit RHS name wins; Foo aliases FooBar
Casing rule (enforced): the inferred name must start with an uppercase letter β the same
isValidConceptNamecheck every other concept-creation surface uses.tc: concept { ... }errors withconcept name 'tc' should start with an uppercase letter. UseTC: concept { ... }. The bare-RHS form is also the only form that infers a name; anonymousconcept { ... }literals nested inside a call argument or array element error cleanly at eval time.
Creating concepts β the canonical surface. A concept
is created with the keyword-first concept form. There is no
create-prefixed, postfix, or is Concept
creation form:
concept Stock # bare
concept Stock "doc" # with a doc string
concept/persist Stock "doc" # with a /persist | /transient | /system refinement
concept Stock { price: 0 } # block form with slot defaults
concept FinKB.Stock "doc" # namespaced
x: a Stock # instance creation, via the indefinite article
Stock extends Asset # class inclusion β
Stock has price: 150 # property (auto-creates Stock on first verb use)
is is a predication operator, never a
creation one β using it to create a concept would conflate two speech
acts (constitutive declaration vs. descriptive predication, the
distinction Russell draws on the copula). So aapl is Stock
classifies an instance, X is Concept (in expression
position) asks "is this a registered concept?", and a statement-level
Stock is Concept is a parser error pointing at
concept Stock.
Define-family form (define concept) β
fills the typed-define slot left open in the existing family of
define axiom / define postulate /
define theorem / define word /
define dialect:
define concept Stock = { # `=` assignment
price: 0,
ticker: ""
}
define concept Bond: { yield: 0.05 } # `:` assignment
define concept Scratch = {} # empty body β bare creation
define/persist concept Pinned = { x: 1 } # refinement: force-persist
define/transient concept Tmp = { x: 1 } # refinement: skip persistence
Syntactically parallel to define dialect: uppercase
name, { ... } body parsed as a hash literal. The
implementation synthesizes an internal ConceptCreation AST
and delegates to the canonical creation pipeline, so every
formation-layer feature (boundary capture, examples auto-classification,
formed_by cross-map, default-grounding cross-map, KB
persistence) carries through transparently. Behaves identically to the
equivalent concept Stock { ... } on every dimension except
syntax.
Mapping to the rest of the family:
| Surface | Speech act | Body shape | Examples in this manual |
|---|---|---|---|
define axiom |
KB claim | bracketed expression | "axiom" section |
define postulate |
provisional claim | bracketed expression | "postulate" section |
define theorem |
derived claim | bracketed expression | "theorem" section |
define word |
Lojban/NSM word | block of slots [...] |
"words" section |
define dialect |
DSL registration | array or {cases:..., vocabulary:...} |
"dialects" section |
define concept |
concept declaration | hash literal { slot: value, ... } |
this section |
v1 limitations:
- Inheritance: no
extends/isslot inline. Follow up withStock is Asseton the next line, or useconcept Stock extends Asset { ... }if you need it co-located. - Defeasible boundary: the
~suffix on hash-literal keys is not parsed (define concept Sage = { boundary~: ... }will fail). Use the canonicalconcept Sage { boundary~: ... }block form, or follow up with the statement-levelSage defines~ { ... }rule. - Namespaced names:
define concept FinancialKB.Stock = ...is not supported (thedefineparser produces a flat[]*Identifiersymbol list). Use the prefixconcept FinancialKB.Stock { ... }for namespaced concepts.
Tests: tests/axioma/concepts/test_define_concept.ax
(happy paths) and the three matching _reject files
(lowercase, /unpersist, non-hash body).
Concept introspection at every level β what
is checks depends on syntactic position:
<Name> is Conceptat statement level (TitleCase LHS) β parser error (useconcept X [doc] [refinement]to create a concept)<Name> is <Parent>β declares a specialization (class-inclusion β)<instance> is <Concept>β classifies the instance (membership β) β canonical<expr> is <C>in expression position β Boolean predication query β canonical (includingX is Conceptas a "is this a registered concept?" query)
Inheritance
concept Animal
Dog extends Animal
Dog has bark: lambda => println("Woof!")
Concept formation layer (Phase 1)
Three slot names on a concept carry first-class
concept-design semantics. They turn
concept Foo { ... } from a plain class declaration into a
contract that pairs prose intent with executable regression tests over
the extent.
concept CFP_Choice # base concept for entities
positive_a: a CFP_Choice {}
positive_b: a CFP_Choice {}
negative_a: a CFP_Choice {}
concept DecisionFatigue
df1: a DecisionFatigue {}
df2: a DecisionFatigue {}
DecisionFatigue has purpose: "Explain degraded choices after repeated decisions"
DecisionFatigue has examples: [df1, df2]
DecisionFatigue has counterexamples: [negative_a]
check DecisionFatigue # β β€α΅ (contract holds)
The four reserved slot names:
| Slot | Semantics |
|---|---|
purpose: |
Prose statement of why the concept exists. Pure metadata, queryable
as Concept.purpose. |
examples: |
Array of ConcreteEntitys that MUST be classified
positively. |
counterexamples: |
Array of ConcreteEntitys that MUST NOT be classified
positively. |
formed_by: |
Closed enum naming the creation mode. Validated at creation time (Phase 2a). |
The formed_by: enum accepts five string values. As of
Phase 2b-1 the cross-map is automatically derived into a
default_grounding slot at concept-creation time, and as of
Phase 2b-2 the derived grounding is actively applied to stored
is-facts at instance-creation and classification time:
formed_by: |
Meaning | Default grounding |
|---|---|---|
"abstraction" |
pattern extracted from multiple observed cases | conjecture (inductive) |
"combination" |
concept synthesized from existing concepts | theorem (derivable) |
"distinction" |
concept split out of a broader one | theorem (derivable from parent) |
"stipulation" |
concept defined by fiat for a purpose | axiom (definitional) |
"metaphor" |
concept formed by mapping one domain onto another | hypothesis (cross-domain) |
concept Monoid {
purpose: "Algebraic structure with associative binary op and identity"
formed_by: "stipulation"
}
concept Smartphone {
purpose: "Phone + computer + camera + internet, unified"
formed_by: "combination"
}
Invalid values are rejected at creation time:
ERROR: concept 'X': formed_by = "stipulashun" is not a recognized creation mode.
Use one of: abstraction, combination, distinction, stipulation, metaphor
The contract is verified by
check Concept (the existing
automated-reasoning check keyword, specialized for Concept
targets). It returns a Belnap B4 value:
| Result | Meaning |
|---|---|
T (β€α΅) |
Every example classifies positive; no counterexample classifies positive |
F (β₯α΅) |
At least one example missing from the extent, or at least one counterexample wrongly classified |
Both (β€β₯α΅) |
The SAME entity appears in both lists (paraconsistent declaration) |
Neither (?α΅) |
No examples and no counterexamples β contract vacuous |
check on a non-Concept target falls through to the
legacy AutomatedReasoningObject wrapper
(check 42 still returns the generic consistency-check
string).
Phase 2b-2 adds the boundary: slot and
the active application of default_grounding. A
boundary: value is a predicate, not an open
expression β it captures the AST before the property eval loop runs and
registers it as a defines classifier scoped to the
concept:
concept P_Person
P_Person has age: 0
P_Person has name: ""
concept P_Adult {
formed_by: "stipulation" # β default_grounding: "axiom"
boundary: age >= 18 and is P_Person
}
alice: a P_Person {name: "Alice", age: 30}
println(alice is P_Adult) # β true (auto-classified)
println(grounding("isa", alice, P_Adult)) # β "axiom"
The stored is-fact inherits axiom grounding (not the
strict-defines default of theorem) because
Stipulation-formed concepts cross-map to axiom. The same
active-grounding flow fires at object-instantiation time:
concept P_Stock { formed_by: "stipulation" } # default_grounding: "axiom"
aapl: a P_Stock {}
println(grounding("isa", aapl, P_Stock)) # β "axiom"
The instance-creation is-fact is gated: a concept
without formed_by: (or explicit
default_grounding:) keeps the pre-2b-2 behavior β the
instance is registered in concept.Instances for
comprehension iteration, but no synthetic is-fact is stored. This
preserves the legacy corpus verbatim.
Phase 3 closes the loop on the Phase 1 contract by
making examples: load-bearing. When the concept has opted
into the formation layer (default_grounding set, either via
formed_by: cross-map or explicitly), the
examples: slot is treated two different ways depending on
whether a boundary: predicate is present:
No boundary β examples become the extent declaration. Each entity in the list is force-classified as a member of the concept, at the inherited
default_grounding. Useful for small enumerated concepts that are easier to list than to describe with a rule:concept VIP { formed_by: "stipulation" # β default_grounding: "axiom" examples: [alice, carol] # auto-classified as VIPs } println(alice is VIP) # β true println(grounding("isa", alice, VIP)) # β "axiom" println(check(VIP)) # β β€α΅ (trivially)With boundary β the boundary owns membership; examples become test cases for the boundary.
check Conceptverifies the boundary classifies every listed example and rejects every counterexample:concept Adult { formed_by: "stipulation" boundary: age >= 18 and is Person examples: [alice, carol] # both β₯ 18 β boundary agrees counterexamples: [bobby] # < 18 β boundary correctly rejects } println(check(Adult)) # β β€α΅ (boundary agrees with examples) concept AdultBad { formed_by: "stipulation" boundary: age >= 18 and is Person examples: [dave] # dave is 8 β boundary disagrees! } println(check(AdultBad)) # β β₯α΅ (example missing from extent)
Counterexamples are never auto-classified β there's no opponent rule
to push against. They still participate in check's negative
verification (no counterexample may be in the extent).
Phase 4 adds the defeasible
boundary form: boundary~:. The tilde parallels the
statement-level defines~ and marks the membership rule as
defeasible β classifications derived from it cap at conjecture-grade,
even on a stipulation-formed (axiom) concept. This expresses a
coherent two-level epistemic stance: the concept itself is definitional,
but specific memberships derived from an uncertain rule remain
tentative.
concept Sage {
formed_by: "stipulation" # concept is axiom-grade
boundary~: age >= 65 and wisdom >= 70 and is Person
}
alice: a Person {age: 70, wisdom: 80}
println(alice is Sage) # β true
println(grounding("isa", alice, Sage)) # β "conjecture" (cap fires)
The cap rule: defeasibility lowers grounding to
conjecture when the concept's
default_grounding is stronger (axiom, postulate, theorem).
When already weaker (hypothesis, datum), the value passes through
unchanged. Declaring both boundary: and
boundary~: on the same concept is rejected at creation
time.
See tests/axioma/concepts/test_concept_formation_phase1.ax for the contract test matrix, tests/axioma/concepts/test_concept_formation_phase2b2.ax for the boundary + active-grounding tests, tests/axioma/concepts/test_concept_formation_phase3_examples.ax for the examples-auto-classification tests, and tests/axioma/concepts/test_concept_formation_phase4_defeasible_boundary.ax for the defeasible-boundary tests.
Phase 5 surfaces the formation layer as queryable
data via the concepts_formed_by(mode_string) builtin.
Returns an array of every concept whose formed_by: slot
equals the given mode, validating the mode against the same enum used at
creation time:
concept Monoid { formed_by: "stipulation" }
concept Group { formed_by: "stipulation" }
concept Penguin { formed_by: "distinction" }
len(concepts_formed_by("stipulation")) # β 2
stips: concepts_formed_by("stipulation")
println(stips[1].formed_by) # β "stipulation"
concepts_formed_by("stipulashun") # β ERROR (typo rejected,
# same enum as Phase 2a)
The return value is a plain Array<Concept>, so it
composes with comprehensions, len, and the rest of the
array vocabulary. Useful for KB audits and for tooling that wants to
render formation-layer choices.
Instances
usa: a Country {}
china: a Country {}
alice: a Person {name: "Alice", age: 30}
Property access
alice.name # Dot
alice's name # Possessive
alice[name -> "Bob"] # ErgoAI frame-form (returns "Bob")
is predicate
alice is Person # true
{X | X is Country} # All instances of Country
{X | X is Country, X.gdp > 20000} # With filter
Cardinality constraints
cardinality(Country, "capital", 1, 1) # Exactly one capital per country
Concept introspection
Stock show properties
Concept display hierarchy
KM-style surface syntax
Four ergonomic forms adopted from Peter Clark & Bruce Porter's KM
2.0 (The Knowledge Machine). Each composes with the existing
concept system above and adds no new semantic machinery β they are
surface forms over the canonical ObjectInstantiation AST
node, concept-level has, and the existing it
keyword.
Instances are created with the natural-language
a/an/entityforms (a Stock {},an Item {},entity Gadget {}) β all three produce the same AST and runtime value. There is noobjectkeyword.
Indefinite-article instance literals β
a Concept {props} and an Concept {props} are
the canonical instance-creation expressions. The property block is
optional. The article is a soft keyword: it only triggers when
followed by a capitalized identifier (Axioma's concept convention), so
variables named a or an continue to work.
mycar: a Car {make: "Toyota", price: 26000}
cat: an Animal {species: "Felis catus"}
cheap: a Car {} # empty-property form
dyn: (a Car {price: 99999}).price # usable mid-expression
fleet: [a Car {price: 10000}, a Car {price: 20000}]
it anaphora β every fresh instance
binds it in the surrounding scope, so subsequent statements
can refer back to it without naming. Method-self semantics (when
it is bound inside an object action) take precedence; the
global binding only fires between consecutive top-level statements.
a Car {make: "Toyota", price: 26000}
println(it.make) # Toyota
mycar: it # capture by name
a Car {make: "Honda"} # rebinds it
println(it.make) # Honda
println(mycar.make) # Toyota (unchanged)
every Concept has prop: val β
universal-slot quantifier; sugar for the existing concept-level
has. Useful when emphasizing intent in literate-style
scripts.
every Car has wheels: 4
every Car has make: ""
c: a Car {make: "Toyota"}
println(c.wheels) # 4 (inherited default)
what is X? /
what is X's Y? β interrogative queries. Evaluates
the operand, pretty-prints <source> is <value>,
and returns the value (so it can be assigned). The trailing
? is required. Slot access via either possessive
('s) or dot notation is accepted.
mycar: a Car {make: "Toyota", price: 26000}
what is mycar? # β mycar is a Car {make: "Toyota", price: 26000}
what is mycar's price? # β mycar's price is 26000
what is mycar.make? # β mycar.make is "Toyota"
KM influence note: Axioma already implements most of KM's deep semantic machinery (frames, inheritance, situations via hypothetical contexts, defaults via defeasible rules, reification, persistence) and goes beyond it on truth representation (B4 bilattice, six-level epistemic grounding, five logic kinds with automatic dispatch). The natural-language surface forms above bring KM's ergonomics in alongside that deeper machinery. See tests/axioma/km/ for the complete test set.
Coreference merging β
unify x with y
Collapses two named entities into one canonical entity with the union
of their slot values. Solves entity resolution β when "Acme Inc." and
"Acme Industries" turn out to be the same company, unify
merges them in place. Composes with B4 paraconsistent truth, six-level
grounding, and atomic transactions.
concept CelestialBody
morning_star: a CelestialBody {visible_at: "dawn"}
evening_star: a CelestialBody {visible_at: "dusk"}
unify morning_star with evening_star # Russell's classical case
println(morning_star == evening_star) # β true
Defeasible: unify~ x with y records the
merge with grounding conjecture (cancellable later).
B4 paraconsistent slot merging: conflicting
single-valued slots keep the canonical's value and record a
Both truth marker. Multi-valued slots get set-unioned.
Transaction-safe: unify inside
transaction_begin() / transaction_rollback()
is reversible β both entities' pre-merge slots and identity are
restored.
Intensional
class descriptions β the Concept where <pred>
A Russell-style definite description with restrictor (KM Β§18.2).
the Stock where price > 1000 denotes the anonymous class
of Stocks whose price exceeds 1000. Composes with is,
comprehensions, and named classes.
concept Stock
Stock has price: 0
luxury: a Stock {price: 80000}
big: the Stock where price > 1000
println(luxury is big) # β true
# Inline membership test
println(luxury is (the Stock where price > 50000)) # β true
# Comprehension over the implicit extent
bigs: {X | X is (the Stock where price > 1000)}
Bare slot names in the predicate resolve to
it.<slot> (same convention as defines
predicate bodies). The intensional class is transient β
it's not registered in the KB, so it adds no permanent vocabulary; use
it for one-off queries or as a slot value when you want to denote a
class without naming it.
Partitions and subsumption
Concept partition Member1, Member2, β¦ declares the
listed subclasses as mutually exclusive. An instance
can be a member of at most one. The disjointness is enforced by
auto-classification β when a defines predicate would
classify an instance into a partition member that conflicts with an
existing membership, the new classification gets the Belnap
both truth value (paraconsistent β no crash).
concept Animal
Bird extends Animal
Mammal extends Animal
Fish extends Animal
Animal partition Bird, Mammal, Fish
robin: a Bird {}
println(robin is Mammal) # β false (disjoint)
A subsumes B is the class-class subsumption infix
(Russell class- inclusion, KM Β§18.3) β true when A is a superclass of
B.
Animal subsumes Bird # β true
Concept subsumes Bird # β true
Bird subsumes Mammal # β false
partitions_of(Concept) returns the declared partition
tuples on a concept.
Querying a partition's live extent
Four builtins make the partition laws β disjointness and exhaustiveness β queryable over a concept's actual instances, returning witness instances (not just a verdict) when a law fails:
concept Thing
UpToUs extends Thing
NotUpToUs extends Thing
Thing partition UpToUs, NotUpToUs
opinion: an UpToUs {}
weather: a NotUpToUs {}
is_partitioned(Thing) # β true (disjoint AND exhaustive over the extent)
partition_overlap(Thing) # β {} (instances in >1 part β empty β disjoint)
partition_gap(Thing) # β {} (extent instances in 0 parts β empty β total)
partition_member(opinion, Thing) # β UpToUs (the part it falls into, or `none`)
mystery: a Thing {} # a bare Thing β in neither part
is_partitioned(Thing) # β false (exhaustiveness now fails)
mystery in partition_gap(Thing) # β true (the gap WITNESS)
This models Epictetus' dichotomy of control as a real partition rather than two hand-rolled relations. Each verdict is open-world β a statement about the facts seen so far; an empty extent is vacuously partitioned.
Description
Logic β concept algebra β β Β¬ β β‘ +
satisfiable
Concepts compose into compound concept expressions
with the DL operators, and a built-in tableau reasoner answers
subsumption, equivalence, and satisfiability questions about them.
β (and) and β (or) and Β¬ (not)
build a first-class ConceptExpr value; β,
β‘, and satisfiable(...) decide.
concept Person
concept Student
concept Employee
Student extends Person
Employee extends Person
ws: Student β Employee # a compound concept (intersection)
@ws # β "ConceptExpr"
Student β Person # β true (subsumption: left is the SUBconcept)
Person β Student # β false
(Student β Employee) β Person # β true (tableau derives it from `extends`)
Student β (Student β Employee) # β true
Student β‘ Employee # β false (equivalence = mutual subsumption)
(Student β Employee) β‘ (Employee β Student) # β true
satisfiable(Student β Employee) # β true
satisfiable(Student β Β¬Student) # β false (the complement clashes)
β vs subsumes β converse
readings. The glyph β reads in the standard DL
direction (C β D means C is the more specific
subconcept β Student β Person is true). The English word
subsumes reads the other way (A subsumes B
means A is the more general superclass β
Person subsumes Student is true). They are converses of the
same relation; pick whichever reads naturally.
| Operator | Glyph | ASCII digraph | Word form | Result |
|---|---|---|---|---|
| conjunction | C β D |
C `sqcap D |
C and/concept D |
ConceptExpr |
| disjunction | C β D |
C `sqcup D |
C or/concept D |
ConceptExpr |
| complement | Β¬C |
β | not C |
ConceptExpr |
| subsumption | C β D |
C `sqsubseteq D |
D subsumes C |
true/false |
| equivalence | C β‘ D |
β | β | true/false |
Β¬ / not is type-dispatched: applied to a
concept it builds the complement; applied to a boolean it is ordinary
logical negation, unchanged. satisfiable reasons over the
TBox assembled from extends declarations; it is for
concepts β boolean/propositional formulas use the separate
is_satisfiable.
The reasoner is the ALC tableau in reasoner/.
Role restrictions, partitions, and extensional membership
A role restriction quantifies a concept over a
binary relation (a role). Use the COLON form
βrole: Concept (some filler is a Concept) and
βrole: Concept (every filler is a Concept);
the role is a native relation.
concept Doctor
concept Cardiologist
Cardiologist extends Doctor
relation hasChild(x, y)
g: βhasChild: Doctor # a ConceptExpr (prints βhasChild.Doctor)
satisfiable((βhasChild: Doctor) β (βhasChild: Β¬Doctor)) # β false (a filler can't be C and Β¬C)
(βhasChild: Cardiologist) β (βhasChild: Doctor) # β true (reasons through the role)
(Β¬(βhasChild: Doctor)) β‘ (βhasChild: Β¬Doctor) # β true (quantifier duality)
A declared partition now becomes real logic:
C partition A, B, β¦ makes the members pairwise disjoint and
jointly exhaustive of C.
concept Animal
concept Cat
concept Dog
Cat extends Animal
Dog extends Animal
Animal partition Cat, Dog
satisfiable(Cat β Dog) # β false (disjoint)
(Cat β Dog) β‘ Animal # β true (covering)
β, β‘, and satisfiable answer
open-world (over declared axioms). Extensional
membership x is <ConceptExpr> answers
closed-world, over the instances and relation facts
actually present:
felix: a Cat {}
felix is (Cat β Β¬Dog) # β true
{p | p <- Animal, p is Β¬Dog} # compound/role concepts in a comprehension FILTER
dora: a Doctor {}
mary: a Cat {} # placeholder unrelated entity
hasChild(felix, dora) # role fillers should be ENTITIES
felix is (βhasChild: Doctor) # β true (some child is a Doctor)
felix is (βhasChild: Doctor) # β true (every child is a Doctor)
(Role fillers must be entities to carry concept membership β a bare string filler is not an instance of any concept.)
Defined concepts, β€/β₯, and more spellings
A defined concept gives a concept a
necessary-and-sufficient body with concept X β‘ <expr>
(the word equivalent works too). The definition is a
genuine axiom in both directions, so subsumptions that follow from it
decide, and membership is read off the body:
concept Person
concept Doctor
Doctor extends Person # so a Doctor is a Person
relation hasChild(x, y)
concept Parent β‘ Person β (βhasChild: Person)
Parent β Person # β true (from the definition)
alice: a Person {}
dora: a Doctor {}
hasChild(alice, dora)
alice is Parent # β true (a Person with a Person child)
Thing (β€) and Nothing (β₯) are built in β
the universal and bottom concepts, available without declaration (you
may still concept Thing "..."; it is an idempotent alias).
Every concept satisfies C β Thing and
Nothing β C; satisfiable(Thing) is true and
satisfiable(Nothing) is false; extensionally every
individual is a Thing and none is a Nothing. A
partition of Thing therefore says the universe is exactly
covered:
concept UpToUs
concept NotUpToUs
UpToUs extends Thing
NotUpToUs extends Thing
Thing partition UpToUs, NotUpToUs
(UpToUs β NotUpToUs) β‘ Thing # β true (the dichotomy is total)
Role restrictions have three interchangeable spellings β the COLON form above, the textbook DOT form, and the OWL Manchester words (role on the left):
(βhasChild.Doctor) β‘ (βhasChild: Doctor) # DOT
(hasChild some Doctor) β‘ (βhasChild: Doctor) # Manchester β
(hasChild only Doctor) β‘ (βhasChild: Doctor) # Manchester β
(The DOT form never disturbs the symbolic quantifier
βx. flies(x), and some/only stay
ordinary words outside this position β some S is P
categorical syntax and an only: binding are untouched.)
A compound concept can also drive a comprehension source for the β/β cases:
{x | x <- (Person β Β¬Doctor)} # the persons who aren't doctors
A bare Β¬C / βR.C / βR.C /
Thing source has no finite extent without a domain
universe, so it is a clean error pointing you at FILTER position
({x | x <- C, x is <expr>}); those
universe-bearing sources are deferred.
The older dl_kb(...) family of builtins remains for
explicit string-keyed knowledge bases. DL operators evaluate in the
tree-walking interpreter (not under --vm, which does not
compile concept declarations).
Enumerated
types β Concept enumerates A, B, C
(Pascal/Ada/Inform-7)
X enumerates A, B, C, β¦ declares X as an enum
concept whose extent is sealed and ordered. Each member becomes
a ConcreteEntity instance of X with ord
(0-based declaration index) and name properties. Members
are bound bare (Mon resolves directly) and also as
concept-qualified (Day.Mon).
Day enumerates Mon, Tue, Wed, Thu, Fri, Sat, Sun
println(Mon) # Mon (inspect renders the bare name)
println(Mon.ord) # 0
println(Day.Mon == Mon) # true (shared identity)
println(succ(Wed)) # Thu (succ/pred also step Integers: succ(5) β 6)
println(pred(Fri)) # Thu
println(Day.first) # Mon
println(Day.last) # Sun
println(len(Day)) # 7
# Ordered comparison uses ord:
println(Mon < Fri) # true
# Iteration walks members in declaration order:
foreach d in Day [
println(d.name)
]
# Strict cross-enum (Pascal/Ada-style):
Color enumerates Red, Green, Blue
# Mon < Red # ERROR: cannot compare Day and Color
Built-ins: succ, pred, len,
Day.first, Day.last. Cross-enum comparison
errors with a clean message (Pascal/Ada strict). Re-declaring a member
name across two enums collides at declaration time β qualify with
Day.Mon to disambiguate. Tests under
tests/axioma/types/.
Integer
subrange types β Concept ranges A..B (Pascal/Ada)
X ranges Low..High declares X as an integer subrange
type. Used in :: annotations, the bound is runtime-checked;
with the --typecheck static pre-pass (Β§26), literal
violations are caught before any code runs.
Digit ranges 0..9
Percent ranges 0..<100 # half-open
d :: Digit : 5 # OK
d :: Digit : 12 # type error
p :: 0..<100 : 50 # inline range annotation
p :: 0..<100 : 100 # type error
# Reassignment honors the binding-time snapshot:
r :: Digit : 3
r = 7 # OK
r = 99 # type error
The by step is rejected in ranges
declarations (membership becomes ambiguous). Inverted bounds
(5..0) and empty half-open (5..<5) also
error at declaration time.
Enum
subrange β Sub extends Enum in From..To (Pascal/Ada)
Workday is Day in Mon..Fri declares a subtype of an
existing enum restricted to a contiguous slice. Every Workday IS a Day
(widening is automatic); a Day with ord outside the slice cannot be a
Workday (tightening is runtime-checked). Member identity is shared β
Mon is still Day.Mon.
Day enumerates Mon, Tue, Wed, Thu, Fri, Sat, Sun
Workday extends Day in Mon..Fri
Weekend extends Day in Sat..Sun
d :: Workday : Wed # OK
d :: Workday : Sat # type error: Sat outside Workday
any :: Day : Wed # widening β every Workday is a Day
# Iteration visits only the slice:
foreach d in Workday [
println(d.name) # Mon Tue Wed Thu Fri
]
println(len(Workday)) # 5
Endpoints must belong to the named parent enum (cross-enum endpoints error). Inverted bounds, integer endpoints with an enum parent, and non-enum parents all error at declaration time.
Slot-metadata helpers β inverse, transitive, find-or-create
Three small builtins covering common KR patterns. All ship as configuration calls; no new keywords.
# Inverse slots β auto-maintain bidirectional relations
concept Car
concept Engine
Car has parts: none
Engine has part_of: none
inverse_slot("parts", "part_of")
car: a Car {}
engine: a Engine {}
car.parts: engine # auto: engine.part_of = car
# Transitive slots β walk a chain in one builtin call
concept Person
Person has parent: none
transitive_slot("parent")
alice: a Person {}; bob: a Person {}; carol: a Person {}
alice.parent: bob
bob.parent: carol
ancestors: transitive_closure(alice, "parent") # [bob, carol]
# find_or_create β definite description with reification
concept Country
Country has name: ""
usa1: find_or_create(Country, {name: "USA"})
usa2: find_or_create(Country, {name: "USA"}) # same instance as usa1
find_or_create is the canonical KB ingestion primitive β
it makes "if this entity already exists, give me it; else make it" a
one-liner. Inverse-slot propagation is one-step (the propagation guard
prevents recursive inverse-of-inverse loops). Cycle-safe entity
rendering is automatic β ConcreteEntity.Inspect detects
already-visiting entities and emits a short reference.
Auto-classification via
defines
A fourth concept-verb that attaches a membership predicate to a class. Any instance whose slots satisfy the predicate is automatically classified; slot mutations re-evaluate and promote/demote in real time. Faithful to KM Β§17, with Axioma additions (B4 truth, six-level grounding, defeasibility).
concept Person
Person has age: 0
Adult defines { age >= 18 and is Person }
Senior defines { age >= 65 and is Adult }
Sage defines~ { age >= 80 } # defeasible β grounding=conjecture
alice: a Person {age: 30}
alice is Adult # true (auto-classified)
alice is Senior # false
alice.age: 70
alice is Senior # true (re-classified on slot mutation)
alice.age: 5
alice is Adult # false (auto-demoted)
Distinction from has:
Concept has prop: val is unidirectional
(is(x, C) β prop(x, val) β every member has this property).
Concept defines { body } is bidirectional
(is(x, C) βοΈ body(x) β membership iff predicate). See KM Β§17
for the canonical Mexican/Square contrast.
The three logical roles, separately surfaced:
| Role | Direction | Surface |
|---|---|---|
| Slot template (structural) | β | Adult has age: 0 |
| Implication (one-way) | β |
is(X, Adult) ==> voting(X, true) |
| Equivalence (two-way) | βοΈ |
Adult defines { age >= 18 and is Person } |
Predicate body conventions:
- Bare slot names (
age,price) implicitly refer toit.<slot>. is Conceptin prefix position desugars toit is Concept.and/or/notand comparison operators work normally; Belnap values propagate through.
Grounding & truth integration:
- Strict
definesβ grounding=theorem;defines~β conjecture. grounding("isa", instance, Concept)reports the grounding.proof("isa", instance, Concept)reports the derivation chain.- Demoted classifications get
truth("false")(Belnap), preserving provenance for audit. Re-promotion re-fires the predicate cleanly. - Belnap
Bothresults from predicate body propagate to the is fact's truth without crashing (paraconsistent semantics).
Auto-create: if the named concept doesn't exist when
defines is evaluated, it's created automatically (extending
root Concept). No need for a separate
concept Adult line.
Value constraints β
constrain()
Write-time validation on slot writes. Where defines
classifies instances based on their slot state,
constrain gatekeeps the slots themselves: register
a predicate that every subsequent write must satisfy, and bad writes are
silently rejected (the slot retains its prior value) with a warning to
stderr.
concept Customer
Customer has age: 0
Customer has email: ""
constrain(Customer, "age", lambda v => v >= 0 and v <= 150)
constrain(Customer, "email", lambda v => contains(v, "@"))
alice: a Customer {age: 30, email: "[email protected]"} # accepted
bad: a Customer {age: -5} # WARN; age stays at default 0
alice.age: 200 # WARN; alice.age stays at 30
alice.age: 45 # accepted
# Multiple constraints AND
constrain(Customer, "age", lambda v => v != 13)
teen: a Customer {age: 13} # rejected (second constraint)
# Subclass inheritance β every constraint declared on Customer applies to VIP
VIP extends Customer
v: a VIP {age: -1} # rejected
Predicate contract:
- Takes exactly one argument β the value being written.
- Returns a Boolean-ish value (truthy = accept, falsy = reject).
- May reference other slots only via captured closure variables;
cross- slot constraints (predicates referencing
self.other_slot) are better expressed asdefines, which already sees the full entity.
Enforcement sites: every slot-write path is hooked β
a Concept {slot: val, ...}β the per-property initialization loop.entity.slot: valβ direct assignment.entity[slot -> val]β ErgoAI frame-attribute set.
Distinction from neighbouring features:
| Feature | Fires when | On failure |
|---|---|---|
cardinality (B.8) |
Write to single-valued slot already populated with different value | Last-write-wins + violation marker |
defines (KM Β§17) |
Slot mutation triggers re-classification | Promote/demote membership in defined class |
constrain (this) |
Any slot write | Skip write + warning to stderr |
Introspection:
constraints_of(Customer, "age") # β 2 (count of predicates on this slot, walking ancestors)
constraints_of(Customer) # β {age: 2, email: 1} (all constrained slots)
KM mapping: closes the gap with KM 2.0 Β§12 (Value
Constraints) β the must-be-a / must-be facets
β without committing to a specific type-system facet. The predicate body
has the full expression language available, so
must-be Integer where v >= 0 and v <= 150 is just
lambda v => v >= 0 and v <= 150 when the type
check is sugar for an instanceof call.
Test: tests/axioma/concepts/test_value_constraints.ax.
English paraphrases for
why β paraphrase()
KM Β§19.3. Register an English template against a concept; when
why X is Concept fires, render the template instead of the
default structured proof. Closes the gap between Axioma's structured
explanation engine and the regulator-/user-friendly English output that
compliance domains require.
concept Person
Person has age: 0
Person has name: ""
Adult defines { age >= 18 and is Person }
paraphrase(Adult, "{it.name} qualifies as an Adult because their age ({it.age}) is at least 18.")
alice: a Person {name: "Alice", age: 30}
why alice is Adult
# β "Alice qualifies as an Adult because their age (30) is at least 18."
Placeholder grammar:
| Form | Resolves to |
|---|---|
{it} |
The instance β its name slot if present, otherwise
Inspect() |
{it.slot} |
Value of the named slot on the instance |
{unknown} |
Left intact (debugging aid) |
Inheritance: a subclass without its own paraphrase
inherits its parent's. Most-specific wins; the parent-chain walk mirrors
how constrain constraints are inherited.
Fallback: if no paraphrase is registered for the
concept (or any ancestor), why runs the existing
structured-proof rendering. The feature is purely additive β adopt it
for the classes where English output matters.
Composition:
defines+paraphraseβ auto-classification rendered in Englishunify+paraphraseβ the canonical entity's slot values are resolved before placeholder substitution- Subclass +
paraphraseon parent β subclass inherits the parent's template until it registers its own
Test: tests/axioma/concepts/test_paraphrase.ax.
Defined instances β
Concept identified by ...
KM Β§17.3 β automatic coreference by identity key. Where
defines auto-classifies instances by their slot
state, defined instances auto-merge instances by a declared
identity key. Declare which slot(s) uniquely identify an individual, and
any two instances of the concept with matching key values are
automatically unified β reactively, on every slot write, with no
explicit unify call.
The recommended canonical surface is the
has/identity slot refinement β identity-ness is slot
metadata, so it belongs on the slot declaration rather than in a
separate statement that re-names the slot. It parallels Axioma's
existing keyword refinements (declare/persist,
axiom/transient, is/same).
concept Customer
Customer has/identity ssn: "" # ssn is the identity key
Customer has name: ""
c1: a Customer {ssn: "123-45-6789", name: "Alice Smith"}
c2: a Customer {ssn: "123-45-6789", name: "A. Smith"}
println(c1 == c2) # β true (auto-unified)
println(c1.name) # β A. Smith (newest write wins)
# Composite key β mark each participating slot; all /identity slots
# on a concept jointly form ONE key (declaration order preserved)
concept Order
Order has/identity customer_id: ""
Order has/identity order_number: ""
Order has total: 0
Alternative surfaces (all valid, all lower to the same registry, nothing deprecated):
Customer identified by ssn # statement form
Order identified by customer_id, order_number # composite, explicit grouping
define_equivalence(Customer, "ssn") # functional builtin
It is the automatic dual of the two manual coreference primitives:
| Primitive | Trigger | Effect |
|---|---|---|
find_or_create(C, {...}) |
explicit call | return existing match or make fresh |
unify x with y |
explicit call | merge two named entities |
has/identity slot refinement |
every slot write | auto-merge any two C instances with matching key |
Semantics:
- Each
has/identityslot is appended to the concept's key in declaration order; multiple such statements accumulate into one composite key (re-declaration is deduped/idempotent). - An incomplete key (key slot missing, or holding
"") never matches β a partially-populated instance is not a dedup candidate yet. A later write that completes the key triggers the merge. - The triggering entity wins (newest write wins on slot conflict); the
pre-existing match redirects via
Canonical. - Reuses the full
unifymachinery β B4 paraconsistent slot-merge, transaction rollback,strength/proof/why. - The merged-away loser is removed from the concept extent, so
{X | X <- Concept}counts distinct identities, not raw records. - Subclasses inherit the parent's key.
Introspection:
equivalence_key_of(Concept) returns the key slots as an
array of strings (parent chain walked), [] if none.
KM mapping: closes KM 2.0 Β§17.3 (Defined Instances β
Testing for Equivalence), the equivalence half of KM's
automatic-classification duality. The membership half (Β§17.2 Defined
Classes) is Axioma's defines.
Test: tests/axioma/concepts/test_defined_instances.ax.
inspect
/ see β identity-passing evaluate-and-display
A prefix directive that evaluates an expression, prints
<source> = <value> to stdout, and returns the
value unchanged. Modelled on Elixir's IO.inspect / Julia's
@show β the prevailing non-imperative idiom for inline
inspection.
c: a Car {price: 150}
inspect c.price # prints "c.price = 150"
inspect c is Car # prints "(c is Car) = true"
# Identity-pass: composes inside expressions
n: inspect expensive() # binds n; the value also printed
Lowest-precedence prefix, so inspect fred is Person
consumes the whole is expression without parens.
see is a true alias β same token, same
semantics. see c.price is identical to
inspect c.price; use whichever reads better in context. The
lexer maps both inspect and see to the
INSPECT token, so there's one implementation.
The conformance harness (tests/km-conformance/) uses
inspect instead of println boilerplate, making
the Axioma side structurally parallel to KM's ;;CHECK +
form, and the harness compares the two ordered value lists
positionally.
Test: tests/axioma/concepts/test_inspect.ax.
Cumulative
slots β Concept has slot/cumulative: val
KM-conformant additive slot inheritance. By default
Axioma overrides an inherited slot when a subclass redeclares
it. The has/cumulative slot refinement opts a declaration
into KM's semantics: the newly declared value is
unioned with the inherited value into a set. Override
(default) and accumulate (opt-in) coexist.
concept Animal
Animal has legs: 4
Dog extends Animal
Dog has legs/cumulative: 3 # union with inherited 4
println((a Dog).legs) # β {3, 4}
Cat extends Animal
Cat has legs: 3 # plain decl β override
println((a Cat).legs) # β 3
The refinement attaches to the slot name
(slot/cumulative), parallel to has/identity
but on the slot. Semantics:
- The union is computed at concept-declaration time, so
ismust precede the cumulativehas(the canonical order). - A cumulative slot is always set-valued; set operands are flattened so repeated cumulative declarations down a taxonomy accumulate into one flat set; duplicates drop.
- Slots not declared
cumulativekeep the default override behaviour.
KM mapping: closes the slot-inheritance gap found by
the KM-conformance harness (tests/km-conformance/ case 06) β
KM unions slot values across a taxonomy, and has/cumulative
lets Axioma reproduce that while keeping override as its default.
Test: tests/axioma/concepts/test_cumulative_slots.ax.
14. Logic Programming (Prolog-Like)
Axioma provides Prolog-like relational programming through set-based deterministic queries β no backtracking, complete result sets, mathematical foundations.
Facts
Declare a relation with relation, then assert facts with
assert (or the graded axiom /
postulate β see Β§15):
relation parent(x, y)
relation age(x, n)
relation works(x, role)
assert parent("John", "Mary")
assert parent("Mary", "Alice")
assert parent("John", "Bob")
assert age("Alice", 25)
assert works("John", "Engineer")
Note: an undeclared bare call like
parent("John", "Mary")at statement position is read as a function call and errors withUndefined word: parent. Declare the relation first (after which a bareparent(...)call adds a fact), or prefix withassert. As a shorthand, a bare header whose arguments are all uppercase-initial and unbound βpair(X, Y)β is itself read as a relation declaration (the optional-relationform). A bound TitleCase argument is a real thing (a type Concept, a declared relation,Thing/Nothing), so calls likedoc(Integer),tableform(Edge), orsatisfiable(Doctor)keep their call semantics.
Pattern queries
{X | X <- parent(X, _)} # All parents
{Y | Y <- parent("John", Y)} # John's children
{(X, Y) | parent(X, Y)} # All parent-child pairs
Filtered queries
{X | X <- parent(X, _), age(X, A), A >= 18} # Adult parents
Cross-relation unification (set ops)
parents: {X | X <- parent(X, _)}
workers: {X | X <- works(X, _)}
working_pars: parents β© workers
Variable chain unification
# Equivalent to Prolog: grandparent(X,Z) :- parent(X,Y), parent(Y,Z)
johns_kids: {Y | Y <- parent("John", Y)}
johns_grands: {Z | Y <- johns_kids, Z <- parent(Y, Z)}
Complex term matching
relation person(x, attr1, attr2)
relation location(x, attr1, attr2)
assert person("John", ("age", 30), ("job", "engineer"))
assert location("John", ("city", "NYC"), ("country", "USA"))
{X | X <- person(X, _, ("job", "engineer"))} # All engineers
{X | X <- location(X, ("city", "NYC"), _)} # NYC residents
Relations as first-class values
A relation name resolves to a first-class Relation value
(not its fact-adder builtin), so it is type-introspectable and
iterable over its extent β symmetric with how a concept
name iterates its instances:
relation parent(x, y)
assert parent("John", "Mary")
assert parent("John", "Bob")
type(parent) # β "Relation" (@parent is the same)
for (x, y) in parent [ ... ] # iterate facts directly β no comprehension
{(X, Y) | (X, Y) <- parent} # bare relation as a comprehension source
{x | x <- node} # unary relation β bare elements
parent("Alice", "Carol") # STILL callable β adds a fact
A predicate's rules are introspectable too.
rules_of(pred) returns an Array of
RuleClause values, completing the F-logic concept β
relation β rule arc (concepts iterate instances, relations iterate
facts, rules are inspectable as data):
relation edge(x, y)
path(X, Y) :- edge(X, Y)
path(X, Y) :- edge(X, Z) and path(Z, Y)
rs: rules_of("path") # Array of 2 RuleClause
type(rs[1]) # β "RuleClause" (rs[1] is RuleClause β true)
rs[1].head # β "path(X, Y)" (.head_name / .name β "path")
rs[1].body # β "edge(X, Y)"
rs[1].defeasible # β false (.strict β true)
rs[1].direction # β "backward" ("forward" for ==> / ~~>)
{c.head_name | c <- rules_of("path")}
Note: because a relation name is now a value, the KB builtins that take a relation (
insert,forget,truth,set_truth,grounding,cancel,challenge, β¦) expect the relation's name as a string βgrounding("parent", "John", "Mary"), notgrounding(parent, β¦). See Β§16 and Β§17.
HiLog meta-queries
Reflective queries over predicate names and arities:
predicates_of("alice") # All facts mentioning alice
predicate_names() # All predicate names with β₯1 fact
?P("tweety") # Prolog-style meta-call: which predicates mention tweety?
?P("alice", ?Y) # Pattern with wildcard in 2nd position
?P(?X, "carol") # Strict arity, per-position unification
15. Knowledge Base, Axioms & Postulates
Declaring knowledge β the six grounding grades
Every stored fact carries an epistemic grounding β how strongly it is held β on this ordered ladder:
axiom > postulate > theorem > conjecture > hypothesis > datum
You declare a fact at a grade with the matching keyword; derivation
and runtime insert produce the rest. Read a fact's grade
back with grounding(rel, argsβ¦).
| Grade | How you express it | Meaning |
|---|---|---|
axiom |
axiom fact |
foundational truth β the highest grade |
postulate |
postulate fact |
a tentative claim that may be refuted |
theorem |
derived by a strict rule (Β§16), or
insert(rel, β¦, "theorem") |
proven from axioms/postulates |
conjecture |
derived by a defeasible rule (Β§16), or
insert(rel, β¦, "conjecture") |
plausibly inferred; may be defeated |
hypothesis |
hypothesis fact, or
insert(rel, β¦, "hypothesis") |
a working assumption |
datum |
plain assert fact, or insert(rel, β¦) with
no grade |
a bare fact β the floor of the ladder |
axiom parent("Adam", "Cain") # foundational
postulate married("Cain", "Awan") # tentative β may be refuted
hypothesis visited("Cain", "Nod") # a working assumption
assert lives_in("Cain", "Nod") # plain fact β grounding "datum"
grounding("parent", "Adam", "Cain") # β axiom
# theorem / conjecture come from derivation (Β§16); or set any grade at runtime:
insert("parent", "Seth", "Enos", "theorem")
Grounding tracks derivation provenance β a strict
rule yields a theorem, a defeasible rule yields a
conjecture. The derived grade is fixed by the rule
type (strict vs. defeasible), not by the
premises' grades: the tier records how a fact was derived, not
a confidence floor over its inputs (a theorem strictly derived from a
datum-grade premise is still a theorem). See
Β§16 for the
mechanics, and challenge (below) / cancel Β·
force_cancel (Β§17) for
revising graded facts.
Persistence control
axiom/persist gravity = 9.81 # Saved to cascade.db
axiom/transient demo_axiom = 42 # Session-only
postulate/persist working = "..."
postulate/transient debug = "..."
Default: REPL persists, scripts are transient.
The public persistence forms are the /persist and
/transient refinements shown above, plus the
declare persistence forms in Β§3.
The shared SQLite KB
Axioma and Cascade share a
single SQLite database (cascade.db) using the same schema.
Both processes can read/write concurrently.
./axioma --no-kb script.ax # Skip Cascade KB preload (~10Γ faster)
16. Rules, Derivation & Epistemic Grounding
Rule forms
| Form | Direction | Strictness | Produces |
|---|---|---|---|
head whenever body β primary (also
head if body; operator twins: <==, legacy
<=, Prolog :-) |
Backward | Strict | Theorem |
typically head whenever body β primary
(β‘ normally, rule~ head if body,
rule/defeasible; operator twin <~~) |
Backward | Defeasible | Conjecture |
body ==> head |
Forward | Strict | Theorem |
body ~~> head |
Forward | Defeasible | Conjecture |
Strict rules
The primary spelling is the natural one β the rule
connective is the word whenever, so a clause reads as
English and wears its logic honestly: "whenever" is English's
universal conditional, exactly the β-bound Horn head ("there is a path
from X to Y whenever there is an edge from X to Y" β
true of every pair). The other spellings remain first-class:
head if body (Visual Prolog's word, gated β see below),
<== (and legacy <=) and Prolog's
:- in arrow/Prolog dress, and the forward
direction is operator-only (==> /
~~>):
grandparent(X, Z) whenever parent(X, Y) and parent(Y, Z) # primary
grandparent(X, Z) if parent(X, Y) and parent(Y, Z) # if-form (gated)
grandparent(X, Z) <== parent(X, Y) and parent(Y, Z) # operator twin (also <= / :-)
parent(P, Q) and parent(Q, R) ==> grandforward(P, R) # forward β operator only
All spellings store the same clause and share the fixpoint engine, multi-clause accumulation, theorem grounding, and safety diagnostics:
path(X, Y) whenever edge(X, Y) # primary natural spelling
path(X, Y) whenever edge(X, Z) and path(Z, Y) # recursive clause
path(X, Y) :- edge(X, Y) # same rule, Prolog dress
mortal("socrates") whenever human("socrates") # ground head β works bare
whenever is a soft keyword
(whenever: 5 still binds a variable) with no conditional
reading, so the shape head(args) whenever body is a rule by
fiat β no gate, any argument casing, ground heads included. The
if spelling is different: because EXPR if COND
is also the postfix conditional modifier
(cancel(d) if challenged(d)), bare if reads as
a rule only when the head call's arguments include an uppercase
logic variable and the head does not resolve
to a callable β push(s, V) if V > 3 keeps its
conditional meaning (push is a builtin), while
path(X, Y) if edge(X, Y) stores a rule clause. With the
explicit rule prefix there is no gate for either word:
rule H(args) whenever body and
rule H(args) if body are always rules. Both bare natural
forms map to the strict reading only; for the
defeasible natural spelling see typically in the next
subsection.
Defeasible rules
A defeasible rule states a default β an
inference that holds unless overridden. Where a
strict rule's conclusion is a theorem that
always follows, a defeasible rule's conclusion is a
conjecture: a plausible default that can be
defeated by a stricter rule, by conflicting evidence, or by an
explicit cancel. This is how Axioma expresses "typically /
normally / by default" reasoning (non-monotonic logic) β birds
typically fly, even though penguins don't. And that literature
gloss is the primary spelling: the statement prefix
typically (synonym twin: normally) marks the
clause defeasible, in Reiter's own words:
typically flies(X) whenever bird(X) # PRIMARY β "birds typically fly"
normally sings(X) if bird(X) # synonym twin; composes with if too
The operator twins write the same default in arrow dress:
flies(X) <~~ bird(X) # backward: flies(X) holds by default when bird(X) does
bird(X) ~~> flies(X) # forward: the same default, written premise-first
(The infix use of the same word β
birds typically fly β is the separate default-logic engine
with Reiter extensions, unchanged; the rule marker is the
statement-start position only.)
The rule prefix offers two further equivalent markers,
both lowering to the same clause as <~~:
rule~ flies(X) whenever bird(X) # tilde marks defeasible (like defines~ / unify~)
rule/defeasible flies(X) if bird(X) # refinement spelling (like concept/persist)
rule/strict walks(X) if bird(X) # explicit spelling of the strict default
Every marker composes idempotently with a matching operator
(typically chirps(X) <~~ bird(X) and
rule~ sings(X) <~~ bird(X) are fine), while a
contradiction such as typically p(X) :- q(X) or
rule~ p(X) :- q(X) is a loud parse error ("conflicting rule
strength") β an explicit defeasibility marker never silently stores a
strict clause.
Both operator forms express the same rule and
produce conjecture-grade conclusions; they differ only
in reading direction β <~~ writes the conclusion first,
~~> the premise first (see the rule-forms table above).
Their strict twins <= (backward) and ==>
(forward) instead produce theorems:
| Strict (β theorem) | Defeasible (β conjecture) | |
|---|---|---|
| backward (conclusion β body) | <= |
<~~ |
| forward (body β conclusion) | ==> |
~~> |
A defeasible conclusion is overridden when a strict
rule proves the contrary, or by a cancel /
force_cancel directive (Β§17) β and
grounding-aware cancel refuses to defeat a theorem. So
flies("tweety") is a conjecture you can retract, while a
theorem stands until you revise its premises.
Frame-logic rule bodies
Bodies can mix is, function calls, attribute paths, and
conjunctions:
TradeLink extends Concept
TradeLink has source
TradeLink has target
{tl | tl is TradeLink, tl.target.gdp_billion > tl.source.gdp_billion * 2}
Epistemic grounding (six ordered grades)
axiom > postulate > theorem > conjecture > hypothesis > datum
- axiom β declared with
axiom. Foundational. - postulate β declared with
postulate. Tentative. - theorem β derived by a strict rule from axioms/postulates.
- conjecture β derived by a defeasible rule.
- hypothesis β user-marked working assumption.
- datum β a plain
asserted fact, or a runtimeinsert(...)without explicit grounding β the floor of the ladder.
Strict rules produce theorems, defeasible rules produce conjectures β
the derived grade is fixed by the rule type, recording
derivation provenance rather than a min(body_groundings)
confidence floor. A theorem strictly derived from a
datum-grade premise is still a theorem, not a
datum.
The introspection builtin is
grounding, and the floor grade isdatum.
Grounding & Kind as first-class values
grounding(...) and truth_kind(...) return
typed values, not bare strings. A Grounding is
ordered (the ladder above); a Kind is a
flat five-member set (logical /
empirical / transcendental /
motive / metalogical). Both coerce against a
plain String, so existing == "axiom" comparisons keep
working.
relation edge(x, y)
axiom edge("a", "b")
path(X, Y) :- edge(X, Y)
g: grounding("edge", "a", "b") # β axiom (a Grounding value)
type(g) # β "Grounding" (g is Grounding β true)
grounding("edge", "a", "b") >= "conjecture" # axiom >= conjecture β true (ordered)
grounding("edge", "a", "b") >= grounding("path", "a", "b") # axiom >= theorem β true
relation color(x, y)
axiom/empirical color("apple", "red")
k: truth_kind("color", "apple", "red") # β empirical (a Kind value)
type(k) # β "Kind"
k == "empirical" # β true (String coercion β no ordering on Kind)
The possessive accessors agree with the builtins:
fact's grounding and fact's kind return the
same Grounding / Kind value types.
Bilattice truth propagation
Every stored fact carries a Belnap B4 value (T,
F, Both, Neither) that propagates
through Horn-clause bodies via lattice meet. Paraconsistent
contamination (Both) survives derivation.
set_truth("parent", "John", "Mary", "true")
truth("parent", "John", "Mary") # Returns Belnap value
Provenance & introspection
The relation is named by string (a relation name is now a first-class value β Β§14):
grounding("parent", "John", "Mary") # a Grounding value: axiom / theorem / conjecture / datum / ...
proof("grandparent", "John", "Alice") # Walks derivation chain back to axioms
why grandparent("John", "Alice") # Prose explanation (keyword form β takes the conclusion call)
rules_of("path") # The predicate's rules as first-class RuleClause values
Challenging axioms
Axioms must be challengeable (Q1 requirement):
challenge("parent", "John", "Mary") # Marks suspect
challenged("parent", "John", "Mary") # Returns true/false
The axiological (value) axis
Orthogonal to epistemic grounding, a fact can carry an explicit value judgment relative to an authority. The axis has three judged stances plus a distinct fourth "never judged" state:
epictetus: agent("epictetus", "Stoic β virtue is the only good")
value_good("courage", "any_agent", epictetus)
value_bad("cowardice", "any_agent", epictetus)
value_indifferent("wealth", "any_agent", epictetus) # the Stoic adiaphoron
value_kind_of("courage", "any_agent") # β "good"
value_kind_of("wealth", "any_agent") # β "indifferent" (a JUDGMENT)
value_kind_of("has_blue_eyes", "any_agent") # β "neutral" (SILENCE β never valued)
facts_by_value_kind("indifferent") # enumerate the judged-indifferent facts
value_indifferent makes "positively judged
neither-good-nor-bad" a first-class stance, distinct from a fact that
was simply never valued (neutral) β the difference between
the Stoic adiaphora and mere silence.
Aspectual facts β qua
qua (Latin "in the capacity of") makes the same
fact viewable under a handle, and rules can key on the
handle β so one fact can carry two value-laden framings with divergent
consequences (intensionality made executable):
relation departed(x)
departed("the_estate") qua "lost"
departed("the_estate") qua "given_back"
framings_of("departed", "the_estate") # β {"lost", "given_back"}
framed_qua("departed", "the_estate", "lost") # β true
disturbs(it) <~~ it qua "lost" # `it` binds to every fact tagged "lost"
at_peace(it) <~~ it qua "given_back"
# the SAME fact is now both disturbing AND at peace β Enchiridion 11
it qua "h" binds it to every fact tagged
h; the relation-anchored form rel(X) qua "h"
instead binds X to the argument. qua stays an
ordinary identifier outside this position (a same-line soft
keyword).
17. Mutation, Transactions & Conflict Resolution
Runtime mutation
The relation is named by string (relation names are first-class values now β Β§14):
insert("parent", "Eve", "Seth") # Defaults to grounding "datum"
insert("parent", "Eve", "Seth", "axiom") # Explicit grounding
forget("parent", "Eve", "Seth") # Retract from all groundings
Note:
deleteandretractare reserved keywords. Useforgetfor the function-call form. The statement formretract [...]is a separate existing feature.
Atomic transactions
transaction_begin()
insert("parent", "X", "Y")
set_truth("parent", "X", "Y", "true")
transaction_commit() # OR transaction_rollback() β undoes ops AND metadata
Defeasible-conflict resolution
relation bird(x)
flies(X) <~~ bird(X)
assert bird("tweety") # Tweety conjecturally flies
cancel("flies", "tweety") # Tweety is a penguin
canceled("flies", "tweety") # true
uncancel("flies", "tweety") # Restore
Canceled facts retain their provenance but are filtered out of comprehensions.
Grounding-aware cancellation
cancel is grounding-aware: it refuses
to defeat a derived theorem (a strict consequence),
returning a non-fatal "refused: β¦" string that points you
at revising a premise (challenge) or overriding with
force_cancel. Defeasible conclusions (conjectures) and base
posits (axioms / postulates / data) stay directly cancelable.
tranquil(P) <== free(P) # strict rule β derives a theorem
assert free("sage")
cancel("tranquil", "sage") # β "refused: β¦ is a theorem β¦" (NOT canceled)
force_cancel("tranquil", "sage") # β "canceled: tranquil(\"sage\")" (explicit override)
forget_cascade
β justification-based retraction (a TMS)
Plain forget retracts a premise but
orphans the conclusions already derived from it.
forget_cascade walks the derivation chains forward,
withdraws the premise and every materialized fact that
transitively depends on it, then lets lazy re-derivation rebuild
whatever still has independent support:
disturbed(P) <~~ judges_bad(P)
assert judges_bad("novice")
{p | p <- disturbed(p)} # β {"novice"}
forget_cascade("judges_bad", "novice")
{p | p <- disturbed(p)} # β {} (the conclusion lifts with its premise)
A conclusion that also follows from a surviving rule re-derives automatically on the next query.
18. Stack-Based Programming
Axioma implements a first-class stack model:
- a first-class
Stackvalue β construct it withs: a Stackor withstack(); both build a realStack(@sβ"Stack").stack(c)additionally converts an array, set, tuple or finite range, with position 1 as the top:stack([1, 2, 3])isStack[1 | 2 | 3], andarray(s)/set(s)/tuple(s)convert back. (stack_new()was the older nullary-only spelling; retired July 2026 β it now errors with a pointer atstack().) - a global interpreter stack, inspectable from the
REPL with
:stack
A Stack value has two equivalent
surfaces β use whichever reads best:
Natural-language β a message verb mutates (statement position) and a possessive reads (expression position):
s: a Stack
s push 5
s push 10
s's top # β 10 (peek; s's depth β 2, s's empty β false)
s's items # β [10, 5] (top-to-bottom)
s pop # remove the top β value discarded in statement position
v: pop(s) # β¦so capture a popped value with the pop() function
Mutating verbs: push pop peek
drop clear dup swap
over rot nip tuck.
Possessive reads: top/peek,
bottom/base,
depth/size/
height/length, empty,
items/elements/contents.
top on an empty stack returns none (check
s's empty first).
Function form (Forth / Pop-11) β every operation is
a function taking the stack as its first argument:
push(s, x), pop(s), peek(s),
dup(s), β¦ These are the same operations as
the verbs above (kept as aliases) and are the form used in the tables
below. (The natural-language surface is evaluator-only; under
--vm, use the function form.)
Core operations
| Op | Effect | Description |
|---|---|---|
stack() |
β s |
Create a new empty stack |
push(s, x) |
β¦ β β¦ x |
Push x onto s |
pop(s) |
β¦ x β β¦ |
Remove and return the top |
peek(s) |
β¦ x β β¦ x |
Return the top without removing it |
depth(s) / stacklength(s) |
β n |
Current number of items |
clear(s) / erase(s) |
β¦ β |
Empty the stack |
Stack-shuffle operations
All take the stack as the first argument and mutate it in place.
| Op | Effect | Description |
|---|---|---|
dup(s) |
a β a a |
Duplicate top |
swap(s) |
a b β b a |
Swap top two |
rot(s) |
a b c β b c a |
Rotate top three |
over(s) |
a b β a b a |
Copy second to top |
drop(s) |
a β |
Discard top |
nip(s) |
a b β b |
Drop second |
tuck(s) |
a b β b a b |
Copy top below second |
pick(s, i) |
β β¦ x |
Copy the element at index i (0 = top) to the top |
roll(s, i) |
β β¦ x |
Move the element at index i to the top |
Bulk & depth operations
| Op | Description |
|---|---|
dupnum(s, n) |
Duplicate top n times |
erasenum(s, n) |
Drop top n items |
Array conversion
| Op | Description |
|---|---|
stack_to_array(s) |
Snapshot the stack as an array, top first |
array_to_stack(arr) |
Build a new stack from an array |
Example
s: stack()
push(s, 1)
push(s, 5)
dup(s) # 1 5 5
swap(s) # 1 5 5 (top two swapped)
println(pop(s)) # 5
println(depth(s)) # 2
println(stack_to_array(s)) # [5, 1] (top first)
REPL inspection
The global interpreter stack is inspected with REPL commands:
:stack # Show stack contents
:s # Alias for :stack
:stack trace # Show stack with type information
:stack depth # Show stack depth
:stack clear # Clear the stack
See tests/axioma/stack/ for comprehensive examples.
19. Three Notations, One Tree
Most languages pick one notation and bury the others under syntactic
surcharge: Lisp commits to prefix, ML to infix, Forth to postfix. Axioma
treats all three as surface forms of the same AST node.
The unifying claim is concrete: for any expression you can write in two
notations, fullform returns the same string.
fullform(2 + 3) # "+(2, 3)" β infix
fullform(+(2, 3)) # "+(2, 3)" β Julia-style prefix
fullform(seq_of(2, 3, +)) # "Sequence(2, 3, +)" β postfix sequence (a different node)
The first two are identical because both parse to the same
InfixExpression. The third is a
SequenceExpression whose stack-reduction semantics happen
to produce the same value.
19.1 Prefix: operators are functions
Every binary operator can be called like an ordinary function. The
parser recognizes op(args) where op is one
of:
+ - * / % # ^ ** .* ./ .+ .- .^
== != < > <= >=
+(2, 3) # 5
*(4, 5) # 20
<(3, 5) # true
+(1, 2, 3, 4) # 10 β left-folded across all args
The variadic form (+(1, 2, 3, 4)) is a left-fold via the
same evalInfixExpression dispatch the infix form uses, so
multi-valued logic dispatch, set operations, and lattice operators all
behave identically. See Β§9.
19.2 Operators as values
A bare operator name evaluates to a synthetic two-argument function. You can bind it, pass it, and compose it like any other callable:
plus: +
times: *
lt: <
plus(2, 3) # 5
reduce(+, 0, [1, 2, 3, 4]) # 10
reduce(*, 1, [1, 2, 3, 4]) # 24
reduce(-, 20, [1, 2, 3]) # 14
The synthetic function has parameters _op_a, _op_b and
body _op_a OP _op_b β the same dispatch path the infix form
uses. Limitation: the synthetic is binary.
(-)(5) returns a partial-application lambda, not the unary
negation; for unary minus on a value use -(x) directly.
19.3 Postfix: comma sequences
A comma-separated chain whose elements include at least one operator
or stack word is parsed as a SequenceExpression and
evaluated by stack reduction:
2, 3, + # β 5
2, 3, +, 4, * # β 20 (push 2, push 3, +, push 4, *)
10, 4, +, 6, 2, -, * # β 56
Sequence semantics: walk the elements left-to-right; numeric/string/array literals push onto a parse-time stack; an operator pops the top two and pushes the result; a stack-shuffle word rewrites the stack (see below). The whole sequence reduces to the single remaining stack value.
A sequence is the right-hand side of an ordinary expression β so it composes:
r1: 2, 3, + # r1 = 5
r2: 2, 3, +, 4, * # r2 = 20
r3: 10, 4, +, 6, 2, -, * # r3 = 56
2, 3, +, . # prints 5 β Forth `.` pops and prints
println(r1) # prints 5 (inside a CALL, commas separate
# arguments: println(2, 3, +) passes THREE
# args β bind the sequence first)
Postfix sequences live inside the expression grammar rather than seizing the whole program, so they coexist peacefully with infix and prefix in the same line.
Multi-bind still works
The parser only collapses a chain to a sequence when at least one element is an operator or stack word. The classic multi-bind pattern is unchanged:
a, b: 5, 6 # a = 5, b = 6 β not a sequence
19.4 Stack-shuffle words inside sequences
Inside a SequenceExpression, the stack-shuffle
vocabulary is in scope and operates on the local parse-time stack rather
than the global interpreter stack from Β§18:
| Word | Effect | Stack effect |
|---|---|---|
dup |
Duplicate top | a β a a |
swap |
Swap top two | a b β b a |
rot |
Rotate top three | a b c β b c a |
over |
Copy second to top | a b β a b a |
drop |
Discard top | a β |
nip |
Drop second | a b β b |
tuck |
Insert top under second | a b β b a b |
20, 4, swap, / # 4 / 20? no β swap before /: 4, 20, / β 0
20, 4, /, 2, + # 20 / 4 + 2 β 7
1, 2, 3, rot, -, + # rot: 2,3,1 β 3-1=2 β 2+2 = 4
These names shadow the same-spelled global-stack operations only inside a sequence; outside sequences the global-stack semantics from Β§18 apply.
19.5 The . (dot)
inspect sigil
A trailing . prints the value of the preceding
expression β a compact "print-and-go" β and works in two positions:
Statement-trailing (after any top-level expression):
2 + 3 . # prints 5
x: 42 . # prints 42
fullform(2 + 3) . # prints "+(2, 3)"
The dot may also sit tight against the expression β
a sentence-style full stop. It works after every literal family and
closer: 2 + 3., 2.5., 10% + 5%.,
$100 + $25., 100x200 + 20x30.,
type(x)., [1, 2]. all print. A .
not followed by a digit never reads as a decimal point, so
$100.50 and 2.5 stay intact while the trailing
dot lexes as the sigil. The one exception: literals whose own syntax
contains dots β URLs, emails, file paths, dates β absorb a tight
trailing dot into the literal (https://example.com. is a
valid trailing-dot FQDN), so use the spaced form . after
those.
Sequence-internal (pops the current top of the
parse-time stack and prints it β the . is consuming, not
duplicating):
2, 3, +, . # prints 5 (stack empty after)
20, 4, /, 2, +, . # prints 7 (stack empty after)
2, 3, +, ., 10, 5, -, . # prints 5, then prints 5 (two checkpoints)
The dot is parsed as the existing PERIOD token wrapped
in a ScopedStatement for the trailing form, and as a
stack-print step for the sequence-internal form. It is interchangeable
with inspect / see in spirit but more
compact.
19.6 Tracing sequence reduction
The existing trace keyword accepts a
sequence (or seq) category that prints a
step-by-step view of stack reduction:
trace sequence
20, 4, /, 2, +, .
# push 20 β [20]
# push 4 β [20, 4]
# / β [5]
# push 2 β [5, 2]
# + β [7]
# . (inspect) β [7]
Bare trace is equivalent to trace all β
every category enables, including sequence. The same
untrace / untrace sequence disable mirror.
The domains
Tracing selects by domain β a whole class of operation β not by named function. Every domain answers to several spellings; an alias always selects the whole domain, never a narrower slice of it.
| Domain | Reports | Also spelled |
|---|---|---|
binding |
every value as it is assigned (: and =
distinguished) |
bindings, assign,
assignment |
comprehension |
set / list comprehensions | setcomp, listcomp |
concepts |
concept declaration and instantiation | concept, object,
instantiate |
control |
if / while / foreach |
if, while, foreach |
epistem |
assert / axiom / postulate /
derivation, with the grounding tier |
the whole ladder β axiom, postulate,
theorem, conjecture, hypothesis,
datum β plus assert, retract,
derive |
func |
function application and closure creation | lambda, closure |
quantifiers |
forall / exists |
quantifier, forall,
exists |
reasoning |
rule firing and inference, including the recursive walk | rule, inference |
relations |
relation queries and unification, with answer counts | relation, unification, query,
queries |
sets |
set operations | set, union, intersection,
difference, diff, symdiff |
stack |
stack / sequence reduction | sequence, seq, push,
pop |
probabilistic |
probabilistic operations | β |
transform |
term transformation | β |
all |
every domain at once (what bare trace means) |
β |
An unrecognized domain is a catchable error naming the recognized set β a typo, a domain that prints nothing, and "the traced operation never ran" would otherwise be indistinguishable.
The epistem domain reports the grounding
tier each fact is written at, which is the thing worth
watching: assert writes datum, the bottom of
the ladder, while assert/axiom routes to the axiom path and
writes at the top.
trace/verbose epistem [ assert likes("alice", "bob") ]
# β Asserting Fact
# arity: 2
# grounding: datum
# relation: likes
# β Fact Asserted (relation): Epistem{type=datum, value="likes(...)", ...} (EPISTEM)
Detail lines are sorted by key, so a /verbose transcript
is byte-reproducible and can be asserted on through
trace_log().
Scoped trace blocks β
trace <domain> [ ... ]
trace has two forms, and the domain list is optional in
both:
| session-scoped | lexically scoped | |
|---|---|---|
| all domains | trace |
trace [ ... ] |
| one domain | trace control |
trace control [ ... ] |
| several | trace func control |
trace func control [ ... ] |
Naming several domains in one statement widens the trace in one go;
untrace func control mirrors it. Every name is validated
before any is applied, so a typo rejects the whole
statement rather than half-enabling it:
trace binding contrl # ERROR: unknown trace domain "contrl" β and
# `binding` is NOT switched on either
The block form traces only its body and restores the previous
state on exit, so a forgotten untrace cannot flood
the rest of the run:
trace binding [ total = 42 ]
outside = 7 # NOT traced β the block restored on exit
println(total) # β 42
Three properties make it composable:
- Additive. A block widens the trace for its
extent; it does not replace what is already on. So
trace control [ ... ]nested inside an activetrace bindingshows both, and leavesbindingrunning afterwards βrestoreputs back the prior state, not "off". - Scope-transparent. Bindings made inside the block survive it. A debugging lens must never change the program it observes; if the block opened a scope, switching tracing on would silently alter the result.
- The transcript survives. As with
untrace, you read the log after the traced region βtrace_log()still returns it once the block has exited. Clear it withclear_trace_log().
Refinements compose, and are restored along with the domain:
trace/verbose binding [ v = 7 ] # v = 7 (INTEGER)
trace binding
w = 8 # w = 8 β /verbose did not survive
untrace
The block is same-line gated, so a [
opening the next line is still its own statement β bare
trace followed by an array literal keeps its old
meaning.
19.7
AST inspection β fullform, treeform,
headof, argsof, hold,
seq_of
The fullform / treeform /
headof / hold family, spelled in Axioma's
call-with-commas idiom. Every builtin in this group has hold
semantics: the argument's AST reaches the builtin unevaluated,
so fullform(2 + 3) prints the tree, not 5.
| Builtin | Returns |
|---|---|
fullform(expr) |
Canonical head(arg, ...) string |
treeform(expr) |
ASCII box-drawing tree |
headof(expr) |
Operator / function / kind of the top node |
argsof(expr) |
Array of fullform-rendered child strings |
hold(expr) |
A *AST value wrapping the unevaluated tree (rebindable,
re-inspectable) |
seq_of(elts...) |
A held SequenceExpression whose elements are the
literal arguments (the postfix counterpart of hold) |
fullform(2 + 3) # "+(2, 3)"
fullform(2 + 3 * 4) # "+(2, *(3, 4))"
fullform(f(x, y + 1)) # "f(x, +(y, 1))"
treeform(2 + 3 * 4)
# +
# βββ 2
# βββ *
# βββ 3
# βββ 4
headof(2 + 3) # "+"
argsof(2 + 3 * 4) # ["2", "*(3, 4)"]
h: hold(a + b * c)
fullform(h) # "+(a, *(b, c))" β auto-unwraps the held AST
s: seq_of(2, 3, +, 4, *)
fullform(s) # "Sequence(2, 3, +, 4, *)"
Auto-unwrap of held AST values. When the argument to
fullform / treeform / headof /
argsof is an identifier whose value is a *AST
(produced by hold or seq_of), the builtin
walks through the binding and inspects the held node. This is what makes
h: hold(2+3); fullform(h) render +(2, 3)
rather than the identifier "h".
19.8 Why "one tree" is the load-bearing claim
The point of the three notations is not stylistic preference. It is
that the evaluator never branches on notation β
2 + 3, +(2, 3), and
reduce(+, 0, [2, 3]) all converge on the same
evalInfixExpression dispatch. Adding a new operator (or
fixing a multi-valued logic semantics bug) touches one path, and every
notation that surfaces that operator inherits the fix.
Concretely, the identity is testable:
verify("identity prefix/infix",
fullform(2 + 3), fullform(+(2, 3))) # both "+(2, 3)"
See tests/axioma/notation/ for the full assertion
battery (operator-prefix, postfix sequence, dot inspect, stack words,
sequence trace, fullform, operator-as-value, three notations) and
tests/axioma/showcase/11_three_notations.ax for a
single-file walkthrough.
19.9 Homoiconicity β building code as data
The inspection family above (fullform /
headof / argsof) lets you read code
as data. The construction family closes the loop: you can build
AST values from scratch in Axioma source, manipulate them, and
ast_eval them back into computation. That's metaprogramming
β programs that produce programs.
Constructors. Each make_* returns an
AST value. Args may be other AST values OR raw Axioma values
(auto-lifted via the same machinery quasiquote uses):
| Builtin | Returns AST of |
|---|---|
make_integer(n) |
IntegerLiteral |
make_float(x) |
FloatLiteral |
make_string(s) |
StringLiteral |
make_boolean(b) |
Boolean |
make_identifier(s) |
Identifier |
make_infix(op, l, r) |
InfixExpression |
make_prefix(op, x) |
PrefixExpression |
make_call(f, args) |
CallExpression |
make_if(c, t, e?) |
IfExpression |
make_lambda([params], body) |
LambdaExpression |
make_array(...) |
ArrayLiteral (variadic or single
Array) |
make_tuple(...) |
TupleLiteral |
make_set(...) |
SetLiteral |
make_sequence(...) |
SequenceExpression (postfix sequence) |
Round-trip identity. Constructed and quoted ASTs compare equal structurally:
make_infix("+", 2, 3) == hold(2 + 3) # true β same canonical form
ast_eval(make_infix("+", 2, 3)) # 5
make_identifier("x") == hold(x) # true
quote(x * y) == make_infix("*", make_identifier("x"), make_identifier("y")) # true
Recursive traversal via parts(ast).
Unlike argsof (which returns strings), parts
returns Array of AST values β so you can walk a tree
without re-parsing at each level:
h: hold((a + b) * c)
outer: parts(h) # [AST(a + b), AST(c)]
inner: parts(outer[1]) # [AST(a), AST(b)]
ast_string(inner[1]) # "a" β use ast_string for inline display
# (fullform has hold semantics; bind first or use ast_string)
is_ast(x) predicates the type β useful
in pattern-matching code:
is_ast(hold(2 + 3)) # true
is_ast(42) # false
The dispatch loop. Recursive AST walks dispatch on
ast_kind:
walk: func(expr) [
k: ast_kind(expr)
if k == "integer_literal" then ...
else if k == "identifier" then ...
else if k == "infix_expression" then ...
else expr
]
The killer demo: symbolic differentiation in 25 lines
Below is a complete textbook differentiator written entirely in Axioma. No interpreter extension required:
diff_infix: func(expr, var_name, dx) [
op: headof(expr)
lhs: parts(expr)[1]
rhs: parts(expr)[2]
if op == "+" then make_infix("+", dx(lhs, var_name), dx(rhs, var_name))
else if op == "-" then make_infix("-", dx(lhs, var_name), dx(rhs, var_name))
else if op == "*" then make_infix("+",
make_infix("*", dx(lhs, var_name), rhs),
make_infix("*", lhs, dx(rhs, var_name)))
else expr
]
dx: func(expr, var_name) [
k: ast_kind(expr)
if k == "integer_literal" then make_integer(0)
else if k == "identifier" then (
if headof(expr) == var_name then make_integer(1) else make_integer(0)
)
else if k == "infix_expression" then diff_infix(expr, var_name, dx)
else expr
]
d: dx(quote(x * x + 3 * x + 5), "x")
println(fullform(d)) # +(+(+(*(1,x), *(x,1)), +(*(0,x), *(3,1))), 0)
# β unsimplified; equivalent to 2x + 3
A simplify(expr) pass (fold 0*x β 0,
1*x β x, x+0 β x) is another function in the
same style; together they make a small CAS.
substitute(expr, var, value) for evaluating at a point is
five lines around make_integer. Everything you'd want from
a computer algebra system is achievable in user-space code.
Definition-time macros
Beyond runtime AST construction, Axioma has Julia/Elixir-style
macros β macro name(params) body
definitions that expand at the call site. Macro arguments arrive as
unevaluated AST, the body must return an AST (typically
built via quasiquote(...) + unquote(...)), and
the expansion is then evaluated in the calling context.
macro double(x) quasiquote(unquote(x) * 2)
double(21) # β 42
# Inspect the expansion without running it:
ast_string(macroexpand(double(21))) # β "(21 * 2)"
# Multi-arg, conditional, nested all work:
macro unless(cond, body) quasiquote(if not unquote(cond) then unquote(body) else none)
macro addAndDouble(a, b) quasiquote((unquote(a) + unquote(b)) * 2)
addAndDouble(3, 4) # β 14
double(double(3)) # β 12 (nested expansion)
Hygiene via gensym:
gensym() # β G__1 (fresh unique symbol)
gensym("tmp") # β tmp__2
Use inside macros that introduce temporaries
(__r: gensym("r")-style) to avoid capturing user
identifiers. Like Julia (and unlike Elixir or Clojure-syntax-rules),
Axioma macros are non-hygienic by default β you opt
into hygiene with gensym.
Implementation: parser at parser/parser.go:9866,
expansion engine at evaluator/macro_expansion.go:187,
macroexpand builtin at
evaluator/evaluator.go:25913. Test corpus:
tests/axioma/metaprogramming/ (10+ files).
The
head / operands / make_expr
normal-form algebra
Mathematica unifies every expression under one shape β
head[args]. Axioma renders its three surface notations
(infix, prefix, postfix) and call syntax to that same normal form, and a
small algebra reads and rebuilds any quoted expression
uniformly, regardless of which syntax produced it.
| Builtin | Returns |
|---|---|
head(ast) |
the operator / functor as a String β infix
"+", a call's functor name, a statement keyword
(":" for a binding, "if", a rule operator
"<~~"), or an atom's kind ("Integer",
"Symbol") |
operands(ast) |
an Array of AST β the arguments with the functor
excluded (the uniform companion to the older
parts, which includes the functor for calls) |
make_expr(head, operandsArray) |
rebuilds the node from a head String + an operands Array β the inverse of the two above |
head('(2 + 3)) # β "+"
operands('(2 + 3)) # β [<AST: 2>, <AST: 3>]
make_expr(head('(2 + 3)), operands('(2 + 3))) # β <AST: (2 + 3)> (round-trips)
head('(add(7, 9))) # β "add" (a call's functor)
head('(if x then 1 else 2)) # β "if"
make_expr(head(e), operands(e)) == e holds for infix /
prefix / postfix / call / binding / rule / if. Fixity is recovered from
a curated operator table; an unknown head builds a
CallExpression.
Pattern
rewriting β match_pattern / subst /
replace_all / rules
The algebra above is the substrate for term
rewriting β Mathematica's expr /. rule and the
heart of a computer-algebra system. Patterns reuse Axioma's existing
?x variable syntax; no new lexer.
| Builtin | Does |
|---|---|
match_pattern(pattern, subject) |
structural match β a Dictionary of bindings, or
none |
subst(template, bindings) |
instantiate a template from a bindings Dictionary |
replace_all(subject, rules) |
bottom-up rewrite to a fixpoint |
rules(p1, t1, p2, t2, β¦) |
flat-pairs sugar for a rule list (the keyword rule is
reserved) |
match_pattern('(?a + ?b), '(2 + 3)) # β {a: <AST: 2>, b: <AST: 3>}
subst('(?a * ?a), {a: '(7)}) # β <AST: (7 * 7)>
replace_all('(x + 0), rules('(?a + 0), '(?a))) # β <AST: x> (additive identity)
A nonlinear pattern (?x + ?x) enforces same-subtree
consistency. Each rule may carry a guard β a third
element, a quoted predicate over the ?vars (Mathematica's
/;); the rule fires only when the guard holds, and guards
see the builtins and the caller's definitions. Pattern
variables in head position (?f(?x)) bind the functor, and
sequence patterns match variable-arity argument runs β
?xs__ (one or more) and ?xs___ (zero or more),
in call-argument position. Together these are enough to write algebraic
simplification and symbolic differentiation as a rule list
rather than a hand-written dispatch.
A form is a collection β the "Julia-Plus" tier
A quoted expression also behaves as the sequence of its operands for the universal collection operations, so generic code traverses code the same way it traverses data:
len('(2 + 3)) # β 2 (was an error before)
'(add(7, 9))[1] # β <AST: 7> (1-indexed into the operands)
[op | op <- '(a + b + c)] # comprehension over a form's operands
foreach op in '(2 + 3) [ println(op) ]
map(func(x) [x], '(2 + 3)) # map / filter accept a form
The code βοΈ data
bridge β to_data / from_data
Two converters cross the line between a quoted AST and an ordinary value:
to_data('(2 + 3)) # β 5 (evaluate a form to its value)
from_data(5) # β <AST: 5> (lift a value into an AST literal)
to_data(from_data(v)) == v # round-trips for scalars, arrays, sets, tuples
from_data understands the set-theory core (Set and Tuple
values lift, not only scalars and arrays). The bridge is the
frictionless path between the two worlds: where the
'(β¦) quote-forms carry bracket-overloading gotchas
('[β¦] is a block quote,
'([β¦]) is an array-literal quote),
from_data(theValue) is unambiguous.
Where Axioma sits in the homoiconicity taxonomy
Per Wikipedia's taxonomy of homoiconic languages, Axioma is in the "weaker tier" alongside Julia, Elixir, and Nim β full toolkit (quote, quasiquote, AST construction, AST evaluation, macros, macroexpand, hygiene primitive) but source code is parsed rather than being a literal data structure.
| Tier | Languages | Defining property |
|---|---|---|
| Purest (S-expressions) | Lisp, Scheme, Clojure, Racket | source = uniform list literal |
| Weaker (data structures for code) | Julia, Elixir, Nim, Axioma | AST as value + macros |
| Other recognized | Mathematica, REBOL, Red, Tcl, Io, Prolog | various |
| eval-on-string only | Python, Ruby, JavaScript | no in-language AST manipulation |
The article explicitly notes "no consensus exists on a precise definition" of homoiconicity β under a lenient definition Axioma qualifies fully; under the strict "source IS data" definition it doesn't. Reaching the strict tier would require abandoning the multi-syntax three-notations design, which is intentional (see Β§19.1βΒ§19.4).
The head / operands /
make_expr algebra and the match_pattern /
replace_all rewriter above add a facility usually
associated with Mathematica β a uniform
head[args] normal form plus structural pattern matching and
term rewriting β onto the reified-AST base. That strengthens the
practical tier (think "Julia-Plus") without changing where Axioma sits
in the strict sense: a quoted form is still a dedicated AST
value, not source-as-list, so '[1, 2, 3] == [1, 2, 3]
remains false. The algebra unifies the operations over
code-as-data; it does not unify the representation.
Reaching the strict "source is data" tier would require a different
language kernel and is deliberately out of scope for the current
design.
See docs/code-as-data.md for comparison tables and
cookbook patterns.
Limits
| Limit | Workaround |
|---|---|
fullform(inline_call(...)) captures the call literally
(hold semantics) |
Bind to local first: r: call(...); fullform(r). Or use
ast_string(call(...)) which doesn't hold. |
ast_eval doesn't see local bindings |
Use quasiquote to splice values into the AST
before evaluating:
ast_eval(quasiquote(unquote(x) + 1)) |
| Macros aren't hygienic by default (Julia-style) | Use gensym("prefix") to generate fresh symbols inside
macro bodies |
quasiquote not yet compiled in --vm |
Run quasiquote-heavy / macro-heavy scripts under the tree-walker |
| No reader macros β can't extend the parser itself | Out of scope for v1 |
| AST nodes are heterogeneous, not Lisp-uniform | Trade-off against rich surface syntax; use ast_kind +
headof + parts to dispatch |
See tests/axioma/notation/test_homoiconic.ax for the
46-assertion smoke test, and demo_differentiate.ax for the
full symbolic-differentiation example.
Grammar classification β is Axioma context-free?
Not as a single fixed context-free grammar. Axioma has a context-free skeleton, but the language the parser actually accepts steps outside CFG-land in one decisive way and two softer ways:
aliasmakes the grammar adaptive (the decisive one). The parser'sparseAliasStatementinstalls the mapping into the live lexer (Lexer.AddAlias), and identifier lexing consults that table on every subsequent token β so afteralias myif = if, the wordmyiftokenizes as theifkeyword for the rest of the same source stream (verified:alias myif = if+alias mythen = then+alias myelse = elsemakesmyif 1 > 0 mythen "a" myelse "b"parse as a full conditional). How a program's suffix parses depends on the content of its prefix. A language whose sentences extend their own grammar mid-stream is not context-free (the same argument that puts Lisp reader macros and Forth word definitions outside CFGs β declared-name βοΈ used-as-keyword dependencies are the classic cross-serial pattern). Note this is keyword remapping, not a general reader-macro facility:aliasre-lexes identifiers onto existing tokens; it cannot introduce new syntactic forms (see the limitations table above).Recognition is PEG-style, not CFG-style. The hand-written recursive descent + Pratt parser carries a dozen-plus
SaveState/RestoreStatespeculative-parse sites βcolonHeadsSetComprehensiontrial-parses to split the British set comprehension{x : x <- s}from a hash literal{k: v};pipeStartsConsTailsplits the cons pattern[H | T]from a comprehension[h | h <- xs];dotBodyIsConceptsplits the DL restrictionβhasChild.Doctorfrom the quantified formulaβx. flies(x). Backtracking with ordered choice is parsing-expression-grammar semantics; PEGs and CFGs are incomparable classes, and "valid Axioma" is defined operationally by what this parser commits to β no equivalent CFG is maintained.Layout and token-shape sensitivity β inelegant but CF-encodable. Dozens of same-line gates make the soft keywords (
mod,div,unless,whenever,qua,only,limit,offset, β¦) operators only between same-line expressions not followed by:; the lexer decides token identity from local context ($5Money vs$nameguard,%dataFile vs7 % 2modulo,#123Issue vs#helloQuoteWord). These could be encoded in a (much larger) CFG by promoting NEWLINE to a terminal and splitting identifier classes, so they don't change the classification by themselves.
What genuinely is context-free: the Pratt expression
core (operator- precedence grammars are a subset of deterministic CF),
all bracket-balanced structure including [python | β¦]
foreign-language blocks (bracket matching is the textbook CFL), nested
string interpolation β and, worth stressing, macros do not break
the surface grammar: unlike Lisp reader macros, Axioma macros
expand post-parse on the AST, so
macro double(x) quasiquote(β¦) never changes how source
tokenizes.
The standard footnote applies as it does to every real language: the
set of programs that run (no undefined words, arity checks, the
uppercase-concept rule, --typecheck's
:: Ordinal discipline) is context-sensitive anyway β true
of C and Python too β which is why the question is conventionally asked
of surface syntax only. Honest one-line classification: a
deterministic context-free core, recognized with PEG semantics, made
formally non-context-free by one deliberate feature (alias)
in the Lisp/Forth adaptive-grammar tradition.
(Runtime-registered dialect grammars don't affect the
surface class β their blocks are just balanced brackets to the parser;
the dialect matching happens at evaluation time.)
20. Russell's Three Meanings of "is"
Axioma distinguishes the three classical meanings of the copula "is":
| Meaning | Syntax | Semantics |
|---|---|---|
| Identity | x is same as y / x is/identical y |
Ontological identity |
| Predication | sky is blue / sky is/property blue |
Property attribution |
| Existence | there is x / exists x |
Existential claim |
hesperus is same as phosphorus # Identity
sky is blue # Predication
there is x in {1, 2, 3}: x > 2 # Existence
Refinement operators: is/same,
is/identical, is/property.
Identity is Leibniz identity, not address comparison
is/same asks whether two expressions denote one
object. It is a refinement of ==, not a
synonym: == asks whether two values denote the same thing,
is/same asks whether they are the same thing.
5 is/same 5 # β true β there is only one 5
1 == 1.0 # β true β same number
1 is/same 1.0 # β false β `type` and `exact?` tell them apart
[1, 2] is/same [1, 2] # β false β two arrays
[1, 2] == [1, 2] # β true β with equal contents
The answer follows Leibniz's two principles, with "property" read as any predicate Axioma can express:
Identity of Indiscernibles β indiscernible therefore identical β holds for immutable scalars: numbers, strings, booleans, dates, URLs and the rest of the REBOL scalars. These are abstract objects with no haecceity; nothing distinguishes one 5 from another, so
5 is/same 5is true.It fails for concrete particulars and containers. Two dogs with the same name are two dogs; two arrays with equal contents are two arrays, because you can push onto one and observe that the other did not change. Identity there is reference identity β which is what makes the Hesperus/Phosphorus case come out right, since two names for one object are identical and the difference of sense belongs to the name:
concept Planet { name: "" } hesperus: a Planet { name: "Venus" } phosphorus: hesperus hesperus is same as phosphorus # β true β one planet, two names twin: a Planet { name: "Venus" } hesperus is same as twin # β false β two planetsIndiscernibility of Identicals β identicals agree on every predicate β runs the other way and is what keeps
is/samefiner than==.1 is/same 1.0is false becauseexact?andtypeare predicates that disagree on them: it is settled by observation, not by fiat.
Two IEEE corners fall out of the same reasoning.
nan is/same nan is true even though
nan == nan is false β no predicate tells one NaN from
another, and identity must be reflexive. -0.0 is/same 0.0
is false even though -0.0 == 0.0 is true β
str renders them "-0" and "0", so
they are discernible. (Scheme's eqv? gives both the same
answers.)
The executable statement of these laws is lib/laws/equality.ax;
run it with axioma lib/laws/equality.ax.
21. Pointers & References
C-style pointers
x: 42
p: &x # Take address
*p # 42 β dereference
*p = 100 # Write through pointer
x # 100
arr: [10, 20, 30]
p2: &arr[2] # Pointer to array element
*p2 # 20
alice: a Person {age: 30}
p3: &alice.age # Pointer to property
*p3 = 31
alice.age # 31
Pointers work in both interpreted and VM modes.
Deep copy
copy(value) returns an independent deep copy:
a: [10, 20, 30]
b: copy(a) # deep copy β mutating b leaves a untouched
Note β
@is type-of, not a reference get-word.@xreturns the type ofx(@42β"Integer"), identical totype(x). Plainxalready yields the value, and the address/dereference operators&x/*p(above) are Axioma's actual reference mechanism.
22. Mathematical Constants & Built-ins
Mathematical constants
| Constant | Value | Description |
|---|---|---|
pi |
3.141592653589793 | Ο |
e |
2.718281828459045 | Euler's number |
phi |
1.618033988749895 | Golden ratio |
sqrt2 |
1.4142135623730951 | β2 |
sqrt3 |
1.7320508075688772 | β3 |
ln2 |
0.6931471805599453 | ln 2 |
ln10 |
2.302585092994046 | ln 10 |
Set constants
| Constant | Description |
|---|---|
emptyset |
{} (glyph β
) |
naturals |
{1, 2, 3, ..., 100} (finite; glyph β) β
lazy β form: infinite_set("naturals") |
integers |
{-50, ..., 50} (finite; glyph β€) β lazy β
form: infinite_set("integers") |
rationals |
β β lazy infinite set, countable/enumerable (glyph
β) |
reals |
β β lazy infinite set, membership-only (glyph β) |
complexes |
β β lazy infinite set, membership-only (glyph β) |
universe |
Universal set for demos |
Mathematical functions
| Function | Description |
|---|---|
abs(x) |
Absolute value |
sqrt(x) |
Square root |
floor(x) |
Floor |
ceil(x) |
Ceiling |
pow(b, e) |
Exponentiation |
sin(x) / cos(x) / tan(x) |
Trigonometry β arguments in radians (like Lua/C);
convert with rad/deg |
asin(x) / acos(x) /
atan(x) |
Inverse trig, radians (asin/acos
domain-checked). atan(y, x) 2-arg is the full-quadrant form
(Lua 5.3+ math.atan) |
atan2(y, x) |
Full-quadrant arctangent β the C/Python spelling of
atan(y, x) (atan2(1, 0) β
pi/2) |
sinh(x) / cosh(x) /
tanh(x) |
Hyperbolic functions |
deg(x) |
Radians β degrees (deg(pi) β 180; Lua
math.deg) |
rad(x) |
Degrees β radians (rad(180) β pi;
sin(rad(90)) β 1; Lua
math.rad) |
quotient(a, b) |
Floor / truncated integer division (same as a // b /
a div b) |
remainder(a, b) |
Remainder of integer division (same as a % b /
a mod b) |
divmod(a, b) |
Combined: returns (quotient, remainder) tuple in one
call |
signum(x) |
Sign as -1 / 0 / 1,
preserving type (IntβInt, FloatβFloat, RationalβInt). A
non-number errors. (sign(x) always returns an
Integer.) |
square(x) |
x * x, preserving numeric type |
add1(x) / sub1(x) |
x + 1 / x - 1 (Lisp 1+ /
1-) |
succ(x) / pred(x) |
Successor / predecessor over the ordinal types (Pascal/Ada
'Succ/'Pred): Integers (succ(5) β
6) and enum members (succ(Mon) β Tue, erroring at the
ends). On Integers β‘ add1/sub1; the
ordinal-typed, enum-symmetric spelling |
isqrt(n) |
Integer floor square root of a non-negative integer (exact, big-int aware) |
numerator(r) / denominator(r) |
Rational accessors; an integer n is n/1
(so denominator(5) β 1) |
to_number(s) |
Parse a string to a number β Integer if integral, else Float; a number passes through |
Randomness β
random / random_seed / shuffle /
sample
Lua's math.random surface, natively:
random() # Float in [0, 1)
random(6) # Integer 1..6 β a die roll
random(10, 20) # Integer 10..20 inclusive
random_seed(42) # select a deterministic stream (returns 42)
random_seed() # entropy opt-in β reseed from the OS, RETURNS the chosen seed
shuffle(xs) # new shuffled array (the input is untouched)
sample(xs) # one random element (arrays, tuples, sets)
sample(xs, 2) # 2 distinct elements, without replacement
values(sample(normal(0, 1), 3)) # distribution draws β values() unwraps a Sample
The seeding model (deliberate). The generator starts
with the fixed seed 1, so every run of an unseeded
program replays the same sequence β reproducibility is the default,
matching the rest of the language (doctests, expect() pins,
the deterministic test sweep). This is the classic Programming in
Lua (β€5.3) model; modern Lua 5.4+ auto-seeds at startup instead β a
deliberate divergence. When you want fresh randomness (a game,
a real simulation), call random_seed() once at the top: it
seeds from OS entropy and returns the seed it chose, so even a "fresh"
run can be logged and replayed exactly. One seedable source backs the
scalar family and the probability-distribution
samplers, so random_seed(42) also makes
sample(uniform(0, 1), n) reproducible.
The long tail. Python's random module
breadth (expovariate, gammavariate, weighted
choices, getrandbits, β¦) stays one spelling
away through the FFI block:
[python/eval | __import__("random").expovariate(1.5) ]
with one caveat: Python-routed draws live in the subprocess's own
generator and do not obey random_seed β
only the native family and the prob distributions join the
seed contract. When a distribution earns a native home, it lands in
prob's seedable source (the
normal/uniform/
binomial/beta path) precisely so it does.
Formatted output β
printf / stringf / format
C-style format verbs; printf writes to stdout,
stringf returns the String (use it inside expressions).
format is an exact alias of
stringf β the spelling Lua, Ruby, Java/C#, and
Rust hands reach for, and in every one of those languages it
returns the string rather than printing (Lua's
string.format, Ruby's format β‘
sprintf; printing stays printf, or compose
println(format(...))). All three interpret escape sequences
in the format and share one verb-aware argument adapter
(also behind the log_*f family):
printf("%05d|%6.2f|%-6s|\n", 42, 3.14159, "ab") # 00042| 3.14|ab |
stringf("%d %d", floor(x), floor(x + 0.5)) # ints under %d
format("%x", 255) # "ff" β exact alias of stringf (returns, never prints)
stringf("%d", 35.0) # "35" β an INTEGRAL Float converts
stringf("%.2f", 1/3) # "0.33" β Rationals format under the float verbs
stringf("%f", 5) # "5.000000" β Integers too
stringf("%s", [1, 2]) # "[1, 2]" β %s/%q take anything (display form)
stringf("%d", 2^100) # big-int aware
stringf("100%% sure") # "100% sure" β %% is the literal percent
stringf("%d", 35.7) # ERROR: %d needs an integer value, got 35.7 β
# use floor(x) or round(x), or format with %g
Verbs: %d %b %o %x %X %c %U (integer class β
%x/%X also hex-dump a String),
%f %e %g + upper (float class), %s %q %v,
%t (Boolean only), %%; flags/width/precision
(%-6s, %05d, %6.2f,
*) follow Go's fmt. The adaptation
rule is the numeric tower's own stance (35.0 == 35
is already true): an integral value converts across the verb
classes; a fractional value under an integer verb is a loud,
catchable error with a rewrite hint β never silent junk. Every
mismatch errors this way (wrong type, missing or extra arguments,
unknown verbs, a bare trailing %), and a Go-style
%!β¦ badge can never appear in output.
Predicates
even(4) # true
odd(3) # true
positive(5) # true
negative(-3) # true
Scheme/Lisp-style
predicates (? suffix)
Lisp/Scheme spell the test marker ? rather than Common
Lisp's p. A trailing ? is part of the
identifier (zero? is one word), so these read naturally.
The sign predicates treat a non-number as false (matching
even/positive).
# sign / parity (over Integer, Float, Rational)
zero?(0) # true plus?(5) # true (strictly positive)
positive?(5) # true minus?(-1/3) # true (strictly negative)
negative?(-5) # true even?(4) # true odd?(3) # true
# type predicates (join number?/integer?/string?/boolean?/null?/pair?/array?β¦)
list?([1, 2, 3]) # true procedure?(func(x) [x]) # true
empty?([]) # true β also "" / set() / {} / dict() ; an infinite set is never empty
symbol?('hello) # true β a lit-word is a symbol ; symbol_name('hello) β "hello"
# float domain (`inf` / `nan` are builtins producing Β±β / NaN floats)
nan?(nan) # true infinite?(inf) # true finite?(3.14) # true
integral?(5.0) # true β the VALUE is whole (5.01/NaN/Β±Inf β false; the TYPE test is integer?)
# equality: eq? / eql? / eqv? are shallow identity (collections by reference);
# equal? is deep, type-strict (the `==` engine)
eq?(1, 1) # true eq?([1,2], [1,2]) # false (distinct objects)
equal?([1,2], [1,2]) # true equal?(1, 1.0) # false (type-strict)
Higher-order functions
map(fn, set) # Transform
filter(pred, set) # Select
reduce(fn, init, set) # Aggregate (left fold)
sum(coll) # Numeric sum over array/set/tuple (Ξ£ is Unicode alias)
# Scheme/Lisp folds & combinators
foldl(fn, init, coll) # left fold, fn(acc, elem) β like reduce (also fold_left)
foldr(fn, init, coll) # right fold, fn(elem, acc) (also fold_right)
append_map(fn, coll) # map then concatenate the per-element collections (flatmap / mapcat)
for_each(fn, coll) # apply fn for side effects; returns null
constantly(x) # β a function that ignores its args and returns x
negate(pred) # β a predicate that logically negates pred (β set `complement`)
List & string helpers
butlast([1, 2, 3, 4]) # β [1, 2, 3] (all but the last)
dedupe([1, 2, 2, 3, 1]) # β [1, 2, 3] order-preserving (remove_duplicates / unique)
chars("abc") # β ["a", "b", "c"] (string β char array)
char_alphabetic?("a") # β true (also char_numeric? / char_whitespace?)
Knowledge-base builtins
rel is the relation name as a string
("parent", not the bare value parent);
args are the fact's arguments.
| Builtin | Description |
|---|---|
insert(rel, args, [grounding]) |
Assert a new fact (default grounding datum) |
forget(rel, args) |
Retract a fact |
set_truth(rel, args, value) |
Attach Belnap B4 value |
truth(rel, args) |
Query Belnap value |
grounding(rel, args) |
Query grounding β a Grounding value (ordered) |
truth_kind(rel, args) |
Query truth-kind β a Kind value (flat) |
proof(rel, args) |
Walk derivation chain |
rules_of(pred) |
A predicate's rules as RuleClause values |
challenge(rel, args) |
Mark axiom as suspect |
challenged(rel, args) |
Check if challenged |
cancel(rel, args) |
Suppress a derived fact |
uncancel(rel, args) |
Remove cancellation |
canceled(rel, args) |
Check cancellation |
transaction_begin/commit/rollback |
Atomic mutation |
predicates_of(X) |
All facts mentioning X |
predicate_names() |
All known predicate names |
cardinality(C, "prop", min, max) |
Register cardinality |
MVL constructors
intuit3("true"/"false"/"unknown")
belnap("true"/"false"/"both"/"neither")
lukasiewicz(0.0..1.0)
23. Venn Diagrams & Visualization
a: {1, 2, 3, 4}
b: {3, 4, 5, 6}
venn(a, b) # 2-set diagram
c: {3, 4, 5}
venn(a, b, c) # 3-set diagram with all intersections
Diagrams auto-pop in the default image viewer and are saved to
diagrams/venn_diagram_TIMESTAMP.png.
Run ./scripts/clean_diagrams.sh to clean accumulated
PNGs.
The
*form family β rendering expressions & relations
A family of introspection builtins renders a value to text.
fullform / treeform show an expression's AST;
tableform / graphform render a
relation's extent (rule-derived facts included, since
they walk the same fact store as comprehensions); tableform
also renders a function's truth table. Relations pass
as a held bare identifier or a string.
| Function | Renders |
|---|---|
fullform(expr) |
AST as a symbolic string |
treeform(expr) |
AST as an ASCII tree |
tableform(rel) |
relation extent as an ASCII table |
tableform(func, [domain]) |
the function's truth table (default domain
{true, false}; name a logic or pass a collection) |
graphform(rel, [fmt]) |
relation as a graph β "ascii" (default) /
"dot" / "cg" / "png" /
"svg" |
relation edge(x, y)
assert edge("a", "b")
assert edge("b", "c")
println(tableform(edge))
# relation: edge (2 facts)
# βββββββ¬ββββββ
# β x β y β
# βββββββΌββββββ€
# β "a" β "b" β
# β "b" β "c" β
# βββββββ΄ββββββ
println(graphform(edge)) # default ASCII adjacency
# graph: edge (2 edges)
# "a" β "b"
# "b" β "c"
graphform(edge, "dot") # Graphviz DOT β paste into `dot -Tsvg`
graphform(edge, "cg") # Sowa conceptual-graph linear notation [a]β(edge)β[b]
graphform(edge, "png") # force-directed PNG β visualizations/graph_<ts>.png
graphform(edge, "svg") # scalable SVG (preferred for web / VS Code)
Headers come from the declared slot names; output tuples are sorted deterministically. Higher-arity relations fall back to positional notation (binary is the natural graph case).
Truth tables β
tableform(func, [domain])
Give tableform a function instead of a relation and it
prints the function's truth table: one row per
assignment of domain values to the parameters, leftmost parameter
varying slowest (the textbook layout). Input columns are headed by the
parameter names and the result column by the function's actual
body expression (p and q β a multi-statement body
falls back to result); a bound function's name appears in
the title. The companion truth_table(f) returns the same
rows as a Set of tuples β truth_table is the data,
tableform the display.
println(tableform(func(p, q) [p and q]))
# truth table: (p, q) (4 rows)
# βββββββββ¬ββββββββ¬ββββββββββ
# β p β q β p and q β
# βββββββββΌββββββββΌββββββββββ€
# β true β true β true β
# β true β false β false β
# β false β true β false β
# β false β false β false β
# βββββββββ΄ββββββββ΄ββββββββββ
The optional second argument selects the value domain β this is how
the multi-valued logic tables print. Name a logic
("boolean", "kleene", "belnap",
"lp", "lukasiewicz", "g3") to
enumerate its canonical value set, or pass an explicit Array/Set for a
general function table:
println(tableform(func(p, q) [p βΌ q], "kleene")) # strong-Kleene NAND, 3Γ3
println(tableform(func(p, q) [p and q], "belnap")) # the full B4 4Γ4 β§ table
println(tableform(func(p) [not p], "g3")) # G3's Β¬U = F, visible
println(tableform(func(x) [x * x], [1, 2, 3])) # function table over a domain
Rows are capped at 4096 (domain^parameters); bodies
dispatch per operand type as usual, so the same βΌ body
prints classical, K3, or B4 tables depending only on the domain.
24. Comments & Literate Programming
Basic comments
# Single-line comment
x: 5 # Inline comment
Markdown documentation blocks
/**md ... */ blocks are extracted by
axiomadoc for HTML/Markdown/PDF rendering:
/**md
# Function: calculateDistance
## Purpose
Euclidean distance between two points.
## Math
$d = \sqrt{(x_2-x_1)^2 + (y_2-y_1)^2}$
## Cross-references
- {@link otherFunction}
- {@concept Point}
*/
calculateDistance: func(x1, y1, x2, y2) [
sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2)
]
Generating documentation
./axiomadoc generate -input . -output docs -format html -template default
./axiomadoc generate -input . -output docs -format html -template academic
./axiomadoc generate -input . -output docs -format markdown
./axiomadoc serve -port 8080 -watch -input . # Live-reload server
./axiomadoc validate -input . -check-links -run-examples
Doctests β examples that test themselves
A documentation example can also be a test, so the examples
in your docs can't silently rot. A fenced
axioma doctest block β written inside a
/**md β¦ */ literate block in a .ax file, or
directly in a .md file β is executed line by line:
| Line shape | Meaning |
|---|---|
expr # β value |
assert that evaluating expr yields
value |
expr # -> value |
same assertion β the keyboard-typeable ASCII spelling of
# β |
expr # raises: substr |
assert that expr errors, with a message containing
substr |
| anything else | setup β run for effect (bindings, declarations, asserts) |
Lines in one fence share an environment; each fence is isolated. The
expected value is compared by canonical value form (so
(14, 2), {1, 2, 3}, "hi",
true compare as values, not as strings).
The fence opens with the info string axioma doctest
(shown here inside a literate block):
/**md
## divmod β quotient and remainder together
```axioma doctest
divmod(100, 7) # β (14, 2)
divmod(10, 0) # raises: by zero
n: divmod(9, 2) # setup line β run for effect
n[1] # β 4
n[2] # -> 1
```
*/
divmod: func(a, b) [(a // b, a % b)]
Run them from the CLI, or with the Playground's β Doctest button:
axioma --doctest path.ax # one file
axioma --doctest docs/ # walk a directory for .ax / .md fences
--doctest prints ok: / not ok:
per assertion and a N passed, M failed summary, exiting
non-zero if any assertion fails or if
no fence is found (so a mistyped axioma doctest tag can't
pass silently). The engine is shared between the CLI and the WebAssembly
build, so the in-browser Playground β
Doctest button gives identical results. For assertions inside
ordinary scripts (rather than docs), see the expect builtin
(Β§26).
25. Interactive Features (REPL)
Line editing
- β/β β command history (persistent across sessions)
- β/β / Home/End β cursor movement
- Tab β keyword completion
Special commands
help # Language help
doc <topic> # Topic documentation
:stack # Inspect interpreter stack
:s # Alias for :stack
:stack trace # Step-by-step stack trace
exit # Quit
doc β statement and
call form
doc is a language statement, not just a REPL command β
it works in scripts too, and it has a call twin that
prints the identical text through the same renderer:
doc Integer # statement form
doc(Integer) # call form β byte-identical output
doc("if") # keywords/operators can't be call arguments β use a String
doc # the full topic catalog (sorted); also doc()
The call form holds its bare-identifier argument un-evaluated
(doc(random) documents the name
random, not the function value), prints like
println, and returns null. It is
evaluator-only β under --vm it rejects with a clear
message.
Runtime reflection β the self-describing surface
Axioma answers its own reference questions at the prompt.
doc <Type> on a built-in primitive type prints a
type card β bounds, literal forms, operations,
conversions, and related functions β and the headline limits are also
live members on the seeded type concepts:
doc Float # the full card: IEEE 754 binary64, literal forms, ==
# pitfalls, conversion family, related functions
Float.max # β 1.79769313486232e+308 (Ruby Float::MAX, native;
# exactly: Float.max == 1.7976931348623157e308)
Float.min_positive # β 4.94065645841247e-324 (smallest subnormal)
Float.epsilon # β 2.22044604925031e-16 (the gap above 1.0)
Float.digits # β 15 (guaranteed significant decimal digits)
Integer.bounded # β false β and asking anyway teaches:
Integer.max # ERROR: Integer has no max β integers are
# arbitrary-precision β¦ see `doc Integer`
Byte.bounded # β true; Byte.min β 0, Byte.max β 255
Cards exist for Integer, Float, String, Boolean, Rational, Complex,
Byte, Bytes, Array, Tuple, Set, and Dictionary. Every card line is
verified by running the interpreter, and doc X β‘
doc(X) stays byte-identical.
Four enumeration/search builtins complete the surface:
keywords() # every reserved word (sorted); keywords("if") β true
builtins() # every builtin function name (sorted; 1000+);
# builtins("map") β true β works under --vm too
concepts() # every Concept visible here β seeded + user-declared
bindings() # what YOU have bound this session (fresh session β [])
apropos("substr") # search names AND documentation text; prints
# name β category β summary lines, sorted
builtins() is precise by design: it lists the standard
(VM-shared) table; env-aware builtins (tableform,
grounding, β¦) are documented individually and surface
through apropos. concepts(),
bindings(), and apropos() walk the live
environment, so they are evaluator-only (clean rejection under
--vm) and shadowable β a user binding of the name wins.
Function signatures are data (the REBOL model β help renders from the spec). Spec-registered builtins carry a runtime-true parameter list, and three introspection functions read it β for user functions too:
signature(round) # β "round(x, [digits])" ([x] = optional, xs... = variadic)
signature(max) # β "max(values...)"
arity(map) # β 2 (fixed arity from the spec; -1 when not one number)
parameters(round) # β ["x", "[digits]"]
adder: func(a, b) [a + b]
signature(adder) # β "func(a, b)" Β· arity(adder) β 2
doc round # the doc card now opens with Signature: and the summary
A builtin without a spec yet renders honestly as
name(...) β the spec table backfills incrementally, and a
drift-guard test pins every registered spec to the live implementation
(its arity claims are exercised against the real argument checking, so
the metadata cannot rot).
Per-type function catalogs β
functions(Type) (Julia's methodswith
view). Each card-bearing type carries a curated, verified list of the
builtins that operate on it (or construct it) β the runtime answer to
"what can I do with this type?":
functions(Integer) # β ["abs", "add1", "bin", β¦, "zero?"] (sorted)
functions("String") # same catalog by name (this spelling works under --vm)
"divmod" in set(functions(Integer)) # β true
functions() # β the cataloged type names (the doc-card set)
Functions only β operators (+, band,
union, β¦) stay documented on the type card, which remains
the home of the full operations story. Every (type, function) pair in
the catalog was verified by running before being listed, and a drift
test pins each entry to a live builtin β so the catalog deliberately
excludes the lookalikes (choose is the binomial
coefficient, not a set picker; drop is the Forth stack
verb; abs rejects Rationals).
Value inspection β describe(x) (the
Elixir i/1 / Common Lisp describe model).
Where doc documents names, describe
inspects values: it prints a card with the display form, the
type, and the type-specific facts you would otherwise probe by hand,
then points at doc <Type> and
functions("<Type>"):
describe(42) # Parity/Sign/Digits + the card pointers
describe("hΓ©llo") # Bytes: 6 (len) vs Runes: 5 (count)
describe(round) # the R3 Signature: + Summary: lines
describe(none) # the none-vs-om teaching card (falsy absence vs truthy Ξ©)
describe(my_relation) # arity + live fact count + tableform pointer
Collections report size and element types; MVL values report their
logic and designation; relations report their extent (which is why
describe is evaluator-only β it reads the live environment;
--vm rejects it with a clear message). It returns
null β the output is the effect β and it is shadowable: a
user binding named describe wins.
Function source β source(f) (the Python
inspect.getsource slot). Where doc documents
names and describe inspects values, source
returns the definition β as a String, not printed, so it
composes:
double: func(x) [x * 2]
source(double) # β "double: func(x) [(x * 2)]" β a re-evaluable statement
eval(source(double)) # the homoiconicity round-trip: re-yields the function
source(func(a, b) [a + b]) # anonymous literal β no binding, no prefix
func zz(0) [1]
func zz(n) when n > 1 [n * 2]
source(zz) # multi-clause: one clausal statement per clause
source(round) # ERROR (catchable) β builtins are Go; see signature/doc
The text is a canonical reconstruction from the live
AST β comments, type annotations, and original formatting are
not preserved (the same printer the '(β¦) quote +
ast_string pair uses). Evaluator-only by construction:
under --vm function bodies are compiled to bytecode and the
AST is gone. Shadowable. Distinct from
sources_of(rel, args...), which is fact provenance
(attesting entities), not code.
methods β the Ruby/Smalltalk spelling of
functions. An exact alias
(methods(Integer) β‘ functions(Integer),
byte-identical including under --vm). The
methods keyword β reserved-but-dead since the
method β action retirement β was un-reserved
in the same round, so it also works as an ordinary identifier, hash key,
and property name again.
Runtime compilation β compile(src).
Compiles a self-contained program (a source String, or
a quoted AST from '(β¦)) to bytecode for the VM once, and
returns a callable that runs it on a fresh VM per call:
c: compile("fib: func(n) [if n < 2 then n else fib(n-1) + fib(n-2)]
fib(25)")
c() # β 75025 β runs the bytecode; call repeatedly
compile('(6 * 7))() # a quoted AST compiles too β 42
try(compile("relation r(x)")) # catchable Error: outside the VM subset
try(compile("x + 1")) # catchable: free identifiers error at
# COMPILE time β never a silent capture
Two deliberate boundaries. The program is a sub-program (the
lang-block model), not a closure over your session: it sees no session
bindings and keeps no state between calls β bake values into the source,
or define and call functions inside it. And it covers the
VM-compilable subset β constructs that are
evaluator-only (relations, rules, set comprehensions, β¦) return a
catchable Error naming the boundary, which makes
try(compile(src)) the first in-language probe of "is this
in the compiled subset?". Results normalize onto the evaluator's values
(a compiled false is falsy, a compiled null is
none). eval runs the full language;
compile runs the compiled subset. Available wherever the VM
is linked (the CLI, both runtimes β under --vm too); the
wasm playground omits the VM and says so.
Runtime parsing β ast(x). The
'(β¦) quote is lexical β it quotes the expression
you are writing. ast() is the runtime front-end:
parse a dynamically-built String without executing it
(eval parses and runs in one step; ast splits
that), pass an AST value through unchanged, or get a user function's
reconstructed definition as an AST:
a: ast("x + 1") # parse, don't run β ast_string(a) β "(x + 1)",
head(a) # the same shape '(x + 1) gives, so the AST
eval(ast("2 + 3")) # algebra (head, ast_type, replace_all) composes
eval(ast("a: 5\na * 2")) # β 10 β a String parses as the WHOLE program
eval(parse("a: 5\na * 2")) # β 5! parse()'s default mode keeps only the
# FIRST statement β ast() never drops code
eval(ast(func(x) [x * 2]))(5) # β 10 β the function-definition twin of source(f)
try(ast("x +")) # parse errors are catchable Errors
A single expression unwraps to its expression node, a single
statement stays a statement, and multi-statement input wraps the whole
Program, so eval(ast(src)) β‘
eval(src) always. The lower-level
parse(code, [mode]) keeps its explicit
"expression"/"statement"/"program"
modes. The String and AST forms work under --vm too;
ast(fn) is evaluator-only by construction (compiled bodies
are bytecode). With this, the reflection wishlist that started the
campaign is fully closed: eval, properties,
arity, bindings, methods,
source, ast, and compile all
exist.
Multi-line
entry β continuation and the dangling else
A line continues (the ... prompt) while a delimiter is
open, after a trailing operator or continuation word (then,
else, and, β¦), or after a trailing
\. One more rule makes the statement-form
if/else typeable line by line: an entry that
already spans multiple lines and parses as a complete if
(or else if chain) with no final
else stays open β because else is
optional, the branch would otherwise evaluate eagerly and a following
else line would be an orphaned SyntaxError. Type the
else to continue, or press Enter on a blank
line to submit the if-without-else
as-is (the Python compound-statement convention):
axioma> if n > 0 then
... println("n is positive")
... else
... println("n is not positive")
n is positive
A single-line if c then e still evaluates immediately.
(Under a pipe the simple line-by-line REPL is used instead β feed
complete constructs per line, or run a file.)
Detecting
interactivity β is_interactive() /
isatty()
A script can ask whether it is attached to a terminal, so the same
source can prompt a human interactively yet stay silent (or take
defaults) when driven from a pipe, a test harness, or CI. Both return a
Boolean:
if is_interactive() then
name: input("Your name? ")
else
name: "anonymous" # piped / non-interactive run
isatty() is the lower-level alias (true iff stdin is a
TTY); is_interactive() is the same check phrased for the
common prompt-or-default idiom. Under a pipe both are
false.
Error handling
axioma> x:
Parser errors:
expected expression after `:`, got EOF
axioma> 5 / 0
ERROR: division by zero
axioma> unknown_function(5)
ERROR: identifier not found: unknown_function
26. CLI Flags, MCP Server & Tooling
CLI flags
| Flag | Effect |
|---|---|
| (none) | Start REPL |
<file.ax> |
Run script |
--vm <file> |
Run in VM mode |
--mcp [start|stop|restart|status] |
MCP server |
--no-kb |
Skip Cascade KB preload (~10Γ faster startup) |
--verbose |
Verbose output |
--language <subset> |
Restrict surface to a subset: axioma/all (default),
axioma/core, axioma/functional,
axioma/knowledge, axioma/knowledge-core,
axioma/beginner |
--mode <name>[,<name>β¦] |
Educational mode: functional, logic,
stack, mathematical, linguistic,
imperative, beginner |
--glyphify <file> /
--asciify <file> |
Token-aware in-place canonicalizer between word operators and
Unicode glyphs (in βοΈ β, and βοΈ
β§, forall βοΈ β; digraphs
`cup β βͺ). Strings/comments untouched;
verifies the rewrite lexes+parses identically before writing (see Β§3
"Typing the glyphs") |
--typecheck (alias --check) |
Static pre-pass: walk annotated code, report type errors, exit non-zero on violations |
--strict |
With --typecheck, also flag undefined-identifier
references |
--run |
With --typecheck, execute the script if (and only if)
the check is clean |
--no-typecheck |
Plain axioma file.ax runs a default non-fatal
warn-pass β the checker walks annotated code and prints
findings as warnings (stderr; a large false-positive count is
summarized, not dumped), then executes regardless (warnings never change
the exit code). This flag turns that warn-pass off. The fatal linter
stays behind --typecheck |
--learn |
Start the interactive learning wizard (single-shot Q&A) |
--learn-list / --learn-task <n> /
--learn-path <dir> |
Wizard navigation + custom lessons |
--recipe |
Start the HtDP-style Design Recipe wizard (six stages per task) |
--recipe-list / --recipe-task <n> /
--recipe-stage <n> /
--recipe-path <dir> |
Recipe wizard navigation + custom recipes |
--annotate / -a
<file> |
Generate a literate walkthrough:
<file>.annotated.md (pure markdown) +
<file>.annotated.ax (executable literate source).
Groups statements into blocks (relation / fact / rule / func / β¦) and
explains each from a pedagogical pattern library |
--annotate-html |
Also emit a self-contained <file>.annotated.html
(embedded CSS, light/dark, inline SVG diagrams for binary
relations) |
--annotate-llm |
Fill gaps the canned patterns don't cover via the configured AI
provider (off by default β -a alone runs offline at
$0) |
--annotate-terse / --annotate-verbose |
One sentence per block / 4β6 sentences with analogies |
--annotate-no-run |
Skip the auto-executed ## Output snapshot embedded at
the bottom |
--doctest <file-or-dir> |
Run the axioma doctest example fences in a file or
directory (.ax / .md); reports
ok: / not ok: and exits non-zero on any
failure or if no fence is found (see Β§24) |
Literate annotation:
--annotate
axioma --annotate path.ax parses the script, groups
consecutive statements of the same kind into blocks, and emits a
step-by-step walkthrough β markdown for reading plus a still-executable
literate .ax. Binary relations with β₯2 facts get an inline
diagram (mermaid in the markdown, inline SVG in the HTML). It runs fully
offline by default; --annotate-llm adds an AI fallback for
unrecognized constructs.
axioma -a prolog-like.ax # MD + AX, offline
axioma --annotate --annotate-html --annotate-verbose script.ax
Educational subset:
axioma/beginner
--language axioma/beginner (or the
#language axioma/beginner source pragma) restricts the
surface to one canonical form per operation β x: value (or
the textbook x = value), func,
if/then/else, while, println,
concept Foo [doc] [refinement] [block],
Day enumerates β¦ β and rejects the alternatives
(lambda, fn, \x ->,
match, repeat, repeatβ¦until,
foreach, is, print,
printf, display, ranges) with
educational guidance pointing at the canonical equivalent. Setting this
subset also auto-activates --mode beginner for the
behavioral overlay (no metaprogramming / FFI / proofs).
Concept declaration in beginner mode. The single
canonical creation surface is concept Foo β bare, with
optional postfix doc string (concept Foo "doc"), prefix
refinement (concept/persist Foo), or slot-defaults block
(concept Foo { slot: default }). The non-canonical
Foo is Concept / Foo exists /
Foo create / create Foo shapes error uniformly
β beginners get the same hint as expert users. is is
canonical for instance classification (apple is Stock) and
Boolean type queries (x is Stock in expression position),
including X is Concept as a "is this a registered concept?"
query.
Binding forms in beginner mode. Both live binding
forms are accepted (x: 5 and the textbook
x = 5 find-or-update), with x: 5 taught as the
canonical form in the HtDP-in-Axioma textbook
(x := 5 is a syntax error). This relaxation trades strict
canonicalisation for compatibility β the textbook teaches one preferred
form while existing tests continue to work unmodified.
Static --typecheck
pre-pass
The checker walks the AST in two passes: pass 1 gathers concept / enum / subrange / function-signature declarations (so forward references work); pass 2 walks every annotated binding, every assignment to an annotated binding, and every call site with literal arguments, emitting errors when both sides are statically known and disagree. Only annotated code is checked β unannotated code stays dynamic.
Nine violation classes covered: annotation/literal mismatch, subrange
over/under bounds, inline subrange, cross-enum literal, reassignment
violation, call-site arg type, arity mismatch, ordinal arithmetic
(below), and (with --strict) undefined-identifier
references.
:: Ordinal β the ordinal discipline. An
Ordinal is an integer used as a position label (a
rank on a value scale, a preference order): it may be compared and
selected β <, >, ==,
min, max, sort, nth
β but never subjected to arithmetic. Adding, differencing, or scaling
ranks is the category mistake the annotation exists to catch, and
assigning an ordinal to an :: Integer binding (laundering
it back into a magnitude) is flagged too. The annotation is
runtime-neutral β at runtime an ordinal is carried by an ordinary
Integer β and applies to bindings and function parameters alike:
rank :: Ordinal: 3
next :: Ordinal: 7
rank < next # fine β comparison is the licensed operation
max([rank, next]) # fine β selection
rank + 1 # --typecheck: arithmetic '+' on ordinal β flagged
better: func(a :: Ordinal, b :: Ordinal) [if a < b then b else a] # fine
axioma --typecheck script.ax # check only
axioma --typecheck --run script.ax # check then run if clean
axioma --check --strict script.ax # alias + undefined-ref detection
Learning wizards
--learn is the single-shot Q&A wizard from
tools/learn/*.json (12 built-in tasks).
--recipe is the multi-stage HtDP-style wizard from
tools/recipes/*.json (5 built-in recipes), walking the
student through data β signature β examples β template β body β tests.
Both wizards inherit --mode and --language
from the parent invocation β
axioma --learn --language axioma/beginner enforces the
beginner subset on student code.
MCP server
Start the server (stdio JSON-RPC) for Claude Desktop / Code / Cascade integration:
./axioma --mcp # Start
./axioma --mcp status # Check
./axioma --mcp stop # Stop
11 tools exposed:
| Tool | Purpose |
|---|---|
parse_axioma |
Parse to AST |
validate_syntax |
Syntax check |
analyze_code |
Semantic analysis |
translate_code |
AST-aware translation (Go, Python, Axioma) |
symbolize_nl |
Natural language β Axioma |
execute |
Run code, return result + output |
query_kb |
Query the persistent KB |
inspect_env |
Inspect session environment |
run_file |
Execute .ax files |
decompose_claim |
Break claim into propositions |
evaluate_b4 |
Belnap B4 / K3 / L3 evaluation |
resolve_entities |
Match entities against KB |
KB location: ~/.axioma/axioma.kb (SQLite). Logs:
~/.axioma/mcp.log. PID: ~/.axioma/mcp.pid.
Claude Desktop config
{
"axioma": {
"command": "/path/to/axioma",
"args": ["--mcp"]
}
}
Other front-ends
- Web GUI β
web-gui/start.sh(Vite/React + Go REST API) - Wails GUI β
wails-gui/(desktop) - Jupyter kernel β
jupyter/install_kernel.sh - VS Code extension β
vscode-axioma/
Testing β the expect
builtin
expect(label, actual, expected) is a real assertion: it
passes iff actual == expected (regular value equality β
cross-type numerics, deep array/set/map, MVL coercion). It prints
go test-style markers β
--- PASS: <label> (<duration>) on a match,
--- FAIL: <label> (<duration>) plus the
actual/expected values on a mismatch; the parenthesized duration is the
time spent evaluating actual/expected
(adaptive Β΅s/ms/s), so a slow
assertion stands out and can flag an optimization regression the way
go test shows per-test durations. A mismatch increments a
run-level counter and continues
(accumulate-and-continue); at end-of-run the CLI prints a Go-style
summary to stderr β PASS <script> (N assertions) or
FAIL <script> (N of M failed) β and exits
non-zero if any expectation failed. (The summary prints
only for scripts that ran β₯1 expect(), so ordinary scripts
stay silent.) This is what lets the parallel test runner (which keys on
exit code) actually detect wrong answers β unlike the older
if cond then println("β
") else println("β") idiom, which
exits 0 even when every check is wrong.
expect("two plus two", 2 + 2, 4) # --- PASS: two plus two (0Β΅s)
expect("oops", 100 // 7, 13) # --- FAIL: oops (0Β΅s) (actual 14, expected 13) β exit 1
expect("slow path", slowfib(30), 832040) # --- PASS: slow path (1.42s) β flags slow code
Because it's a function call (not a keyword), a local
expect: func(...) definition shadows it β so adding it was
zero-regression for the files that previously hand-rolled their own
expect.
For documentation examples that double as tests, see
Doctests (Β§24): axioma --doctest runs the
runnable axioma doctest fences embedded in your
.ax / .md files, and the Playground's
β Doctest button runs them in the browser.
27. Examples
Set theory
numbers: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evens: {2, 4, 6, 8, 10}
primes: {2, 3, 5, 7}
evens union primes # {2, 3, 4, 5, 6, 7, 8, 10}
evens intersect primes # {2}
numbers difference evens # {1, 3, 5, 7, 9}
primes subset numbers # true
venn(evens, primes)
Concept system + frame queries
Country extends Concept
Country has gdp_billion
Country has population_million
usa: a Country {}
usa.gdp_billion: 27000
usa.population_million: 332
china: a Country {}
china.gdp_billion: 18000
china.population_million: 1410
big_economies: {C | C is Country, C.gdp_billion > 20000}
Logic programming with rules
relation parent(x, y)
assert parent("Adam", "Cain")
assert parent("Adam", "Abel")
assert parent("Cain", "Enoch")
assert parent("Enoch", "Irad")
# Strict rule: ancestors
ancestor(X, Y) <= parent(X, Y)
ancestor(X, Z) <= parent(X, Y) and ancestor(Y, Z)
# Query
{Y | Y <- ancestor("Adam", Y)}
# {"Abel", "Cain", "Enoch", "Irad"}
# Provenance
proof("ancestor", "Adam", "Irad")
why ancestor("Adam", "Irad")
Defeasible reasoning
relation bird(x)
assert bird("tweety")
assert bird("polly")
assert bird("opus") # A penguin
flies(X) <~~ bird(X) # Birds typically fly
cancel("flies", "opus") # But opus doesn't
{X @conjecture | X <- flies(X)} # {"polly", "tweety"}
Multi-valued logic
# Belnap B4 β paraconsistent reasoning under contradictions
relation parent(x, y)
assert parent("X", "Y")
set_truth("parent", "X", "Y", "both") # Contradictory source
truth("parent", "X", "Y") # β€β₯α΅
# GΓΆdel G3 β intuitionistic
p: ?β± # β‘ intuit3("unknown")
g3_lem(p) # ?β± β LEM not valid
g3_dne(p) # ?β± β DNE fails
p implies p # β€β± β reflexive
# Εukasiewicz L3 β fuzzy-like truth
half: lukasiewicz(0.5)
half implies lukasiewicz(0.7) # 1.0 (truthier consequent)
Functional programming
data: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
# Sum of squares of odd numbers
result: reduce(
lambda (acc, x) => acc + x,
0,
map(lambda x => x * x,
filter(lambda x => odd(x), data)
)
)
# 1 + 9 + 25 + 49 + 81 = 165
Stack-based RPN
3 4 + 2 * # ((3 + 4) * 2) = 14
:s # [14]
5 dup * # 5Β² = 25
2 swap # [2, 25]
Atomic mutation
relation parent(x, y)
transaction_begin()
insert("parent", "Eve", "Seth")
insert("parent", "Seth", "Enos")
set_truth("parent", "Eve", "Seth", "true")
# Oops, mistake
transaction_rollback() # Undoes all three operations
28. Embedding Other Languages
Axioma can call out to peer languages from inside an .ax
script. The mechanism is built around a single block form and
language-prefixed namespaces (python.math.sqrt,
julia.exec(β¦)) β the prefix is what says a call crosses
into a foreign runtime, while a BARE package name
(math.sqrt) is always native (Β§27.2). The goal is
adoption, not performance: developers coming from
Python (or Julia, R, JS) keep familiar call sites one prefix away while
the rest of the program is written in Axioma, then migrate piece by
piece as native equivalents land.
28.1 The
[<dialect> | β¦ ] block
mean: [python |
xs = [1, 2, 3, 4, 5]
print(sum(xs) / len(xs))
]
println(mean) # 3.0
- The opening
[pythonis a registered dialect tag. Recognized tags:python(aliaspy); the secondary subprocess runnersjulia,rlang(R),js,lua,lisp(Common Lisp via sbcl; aliasescl/commonlisp), the compiled runnerspascal(fpc; aliaspas) andclang(the system C compiler); plus the in-process/translation tags (axioma,monkey,sql,model,solver,sympy/cas,nl). Julia/R/Node are exec-only; Lua and Lisp add/eval(see Β§28.2). - Two families. Most tags are foreign: the
body goes to a runtime you installed, and the block fails on a machine
without it. A few are native β
axioma,monkey,sql,sparql,cypher,logic,solver,modelare implemented inside Axioma itself, need no toolchain, and work in the browser playground.monkey(Β§28.3) is the only one of those that is a general-purpose foreign language rather than a query surface or a lens. - One-letter tags are banned by design β R goes by
rlang(renamed fromr, July 2026) and C byclang, neverc: whilerwas a dialect tag,[r | r <- xs]was not a comprehension (the whole body shipped to Rscript).[r | β¦]and[c | β¦]comprehension heads are ordinary again. - After the
|the parser switches to a raw-source reader: every character up to the matching]is sent verbatim to the foreign runtime. Indentation, embedded brackets, multi-linedefs, and string literals all survive intact. - The block is an expression β its value is the
foreign runtime's captured stdout as an Axioma
String. - Both the tree-walker and the bytecode VM execute the block; switch
with
--vmand behavior is identical.
Mode refinement:
[python/eval | β¦ ]
The default form ([python | β¦ ]) is "exec mode": the
body is run as a Python script, and whatever it prints to stdout becomes
an Axioma String. To get a typed value back without writing
print(...), use the eval mode refinement β the
body is treated as a single Python expression and the result is
JSON-decoded into a typed Axioma value:
n: [python/eval | 2 + 3] # 5 (float)
arr: [python/eval | [x*x for x in range(5)]]
# [0,1,4,9,16] (array)
person: [python/eval | {"name": "ada", "age": 36}]
# ObjectMap
flag: [python/eval | 3 > 2] # true (boolean)
Trade-offs:
| Form | Body kind | Returns | Use when |
|---|---|---|---|
| `[python | β¦ ]` | full script | String |
| `[python/eval | β¦]` | single expression | typed value |
eval mode rejects multi-statement bodies (Python's
eval() only accepts an expression). For a multi-statement
body that should still return a value, fall back to exec and
print(json.dumps(...)) your result.
Sharing scope with Python
Free identifiers in the block body that resolve to a JSON-marshallable Axioma binding are auto-injected as Python locals before the body runs. The result is that the natural form just works:
xs: [1, 2, 3, 4, 5]
factor: 10
mean: [python/eval | sum(xs) / len(xs)] # 3
scaled: [python/eval | [n * factor for n in xs]]
# [10,20,30,40,50]
Capture rules:
- A name is captured if it appears in the body, isn't a Python keyword
or common builtin, isn't a
for X in β¦loop target, and resolves to an Axioma value of a marshallable type (Integer, Float, Boolean, String, Array of marshallable, Tuple, Null). - Names of types we can't safely cross the wire (
Set,ObjectMap,Concept,Reference, MVL values, lambdas) are silently skipped β the body sees nothing for that name and Python errors normally if it tries to use it. - Reassigning a captured name inside the body is fine and has no effect on the Axioma scope. Capture is one-way: Axioma β Python.
- Identifiers inside Python comments and string literals don't trigger capture.
For names you specifically don't want captured (e.g. an Axioma
len that would shadow Python's builtin if it weren't
already on the reserved list), the simplest workaround today is to alias
the value through a name that doesn't collide.
28.2 Secondary runners: Julia / R / Node.js / Lua / Common Lisp / Free Pascal / C
Beyond Python, seven foreign runtimes run as per-call subprocesses.
Each spawns the host once per block and returns its captured stdout as
an Axioma String β exec mode for
Julia/R/Node (no /eval typed-value form yet; they lack a
uniform JSON round-trip). Common Lisp and Lua also offer an
/eval mode ([lisp/eval | β¦ ],
[lua/eval | β¦ ]) that returns the body's value as a typed
object β see their specifics below. Free Pascal and C are the
COMPILED runtimes in the family: the body is compiled
(fpc / the system cc) and the produced binary
is run β see their specifics below. In exec mode the body must
print its result.
| Tag(s) | Runtime | Binary (override env) |
|---|---|---|
julia |
Julia | julia |
rlang |
R | Rscript |
js |
Node.js | node |
lua |
Lua | lua (AXIOMA_LUA) |
lisp (aliases cl,
commonlisp) |
Common Lisp | sbcl (AXIOMA_LISP) |
pascal (alias pas) |
Free Pascal (compile-and-run) | fpc (AXIOMA_PASCAL) |
clang |
C (compile-and-run) | cc (AXIOMA_CC) |
[julia | println(sum(1:10)) ] # β "55"
[rlang | cat(mean(c(1, 2, 3))) ] # β "2"
[lisp | (format t "~a" (+ 1 2)) ] # β "3"
[cl | (princ (reduce (function *) '(1 2 3 4 5))) ] # β "120" (factorial)
[pascal | writeln(2 + 2) ] # β "4"
[clang | printf("%d\n", 2 + 2); ] # β "4"
[lua | print(2 + 2) ] # β "4"
[lua/eval | {1, 2, 3} ] # β [1, 2, 3] (typed Array)
Common Lisp specifics.
- The body runs via
sbcl --noinform --no-sysinit --no-userinit --non-interactive --eval <body>β hermetic (no init files), no herald, debugger disabled, clean non-zero exit on an unhandled condition (the condition text comes back as a catchable AxiomaError). Print withformat/princ/print; a bare(+ 1 2)returns""because--evaldoes not echo a form's value β for a typed value back, use/evalmode (next bullet). - Eval mode β
[lisp/eval | β¦ ](aliases[cl/eval | β¦ ],[commonlisp/eval | β¦ ]) returns the body's value as a typed Axioma object instead of captured stdout:[lisp/eval | (+ 1 4 9) ]β14(Integer),(/ 10 4)β2.5(Float),(list 1 2 3)β[1, 2, 3](Array),(> 5 2)βtrue,nilβnone. CL has no stdlib JSON, so the runner wraps the body in a small self-contained JSON encoder (integers / ratios / floats / strings / symbols /T/NIL/ proper-lists / vectors round-trip; an integer-valued float collapses toInteger, matching[python/eval | β¦ ]). The body's own stdout is discarded in eval mode β only the value returns. An unknown refinement ([lisp/foo | β¦ ]) is rejected without spawning sbcl. AXIOMA_LISPoverrides the binary, but it must be SBCL-compatible β the runner passes SBCL's flags. Use it to pin a specific sbcl (e.g. a Roswell-managed one) or an alternate path, not to switch to clisp/ecl/ccl (whose flags differ). Unset βsbclonPATH.- Body-capture caveats (Lisp-aware reader). The Lisp
dialects use a Common-Lisp-aware raw reader so the quote
'(the QUOTE operator, normally unpaired as in'(1 2 3)) is not mistaken for a string delimiter β[lisp | (mapcar #'1+ '(1 2 3)) ]works. Double-quoted"β¦"strings,;line comments, and#\xcharacter literals are skipped, so a]inside any of them is safe. The residual limits (rare): a bare]inside a|multi-escape symbol|or a#| β¦ |#block comment will still close the block early β avoid those in inline bodies. There is no escaping; the body is passed byte-for-byte. - VM parity: full. Both
[lisp | β¦ ](exec) and[lisp/eval | β¦ ]compile toOpLangBlockand run identically under--vm(the lang tag carries the mode).
Free Pascal specifics.
The Pascal runner exists for a concrete workflow: the decades of data
structures & algorithms books written in Pascal. Type the book's
code in verbatim, run it under fpc, and use its output as
the oracle for an idiomatic Axioma port β differential
testing instead of eyeballing:
original: [pascal |
program qsortdemo;
{ Wirth-style in-place quicksort β verbatim from the book }
β¦
end. ]
ported: str_join(map(func(x) [str(x)], sort(xs)), " ")
expect("port agrees with the Pascal oracle", ported, original)
- The mode slot selects the fpc DIALECT, not an
exec/eval split (Pascal has no value-echo semantics β stdout is the
interface, so there is no
/eval).[pascal | β¦ ]defaults to Turbo Pascal 7 (fpc -Mtpβ the Borland-era book dialect);[pascal/iso | β¦ ]is ISO 7185 standard Pascal (Wirth-era book code,program p(output);headers);[pascal/objfpc | β¦ ]and[pascal/delphi | β¦ ]cover the modern dialects. An unknown mode is rejected without spawning fpc. - Three body shapes. A full
programcompiles verbatim. A headerless fragment ending inend.β declarations plus a main block, the shape book chapters print β gets aprogramheader prepended. Bare statements (writeln(2 + 2)) get a full program/begin/end.skeleton. Aunitbody is rejected with guidance (units compile to libraries, not runnables). Compile errors return as catchable AxiomaErrors carrying fpc's diagnostics, plus a note saying how many lines the auto-wrap offset fpc's line numbers by. - Runtime behavior. The binary runs with stdin at EOF
(interactive
read/readlnbook programs need their inputs replaced with constants), bounded byAXIOMA_PASCAL_TIMEOUTseconds (default 30 β book-code testing is exactly where accidental infinite loops happen). A runtime error (Runtime error 200= division by zero, etc.) comes back as a catchableErrorcarrying the FPC RTL message. - Pascal-aware body reader. Pascal strings are
single-quoted with no backslash escapes β the quote is
escaped by doubling (
'it''s') β sowriteln('C:\TP\BIN')reads correctly;"is not a string delimiter; and{ β¦ }/(* β¦ *)/// β¦comments may contain unpaired apostrophes ({ don't panic }).array[1..10]bounds anda[i]indexing balance the block's bracket depth naturally. Residual limit: a bare unbalanced]outside any string/comment closes the block early. AXIOMA_PASCALoverrides the compiler binary (unset βfpconPATH; macOS:brew install fpc). Intermediates are compiled in a throwaway dir β nothing lands in your project tree.- Compile cache. A successful compile is kept (keyed
on the wrapped source + dialect mode + compiler identity), so re-running
an identical block skips fpc entirely β ~0.8 s down to ~15 ms β and only
re-RUNS the program. That makes repeated test sweeps and book-pass
reruns cheap. Failures are never cached (an error is always freshly
diagnosed). Disable with
AXIOMA_PASCAL_NOCACHE=1; clear by deleting the cache dir (macOS:~/Library/Caches/axioma/pascal). - VM parity: full.
[pascal | β¦ ]and every/modeform compile toOpLangBlockand run identically under--vm.
Lua specifics.
- The body runs via the system interpreter β
luaonPATH(macOS:brew install lua), overridable withAXIOMA_LUA(e.g.luajitor a pinnedlua5.4). Runs are bounded byAXIOMA_LUA_TIMEOUTseconds (default 30 βwhile true do endcannot hang the interpreter), and stdin is at EOF, soio.read()returnsnilinstead of blocking. - Eval mode β
[lua/eval | β¦ ]returns the body's value as a typed Axioma object instead of captured stdout:[lua/eval | 2 + 2 ]β4(Integer),2^10β1024(Lua's float power; whole floats collapse to Integer, matching[python/eval | β¦ ]),1.5 * 3β4.5(Float),"hi" .. "!"β"hi!",10 > 3βtrue,nilβnone, a sequence table{1, 2, 3}β[1, 2, 3](Array), any other table β a Dictionary ([lua/eval | {x = 7} ].xβ7). Lua has no stdlib JSON, so the runner prepends a small self-contained pure-Lua encoder (the Common Lisp precedent). The body must be a single expression; it is parenthesized, so a multi-value expression adjusts to its first value (Lua's own rule). An unknown refinement ([lua/foo | β¦ ]) is rejected without spawning lua. - Body-capture caveats (Lua-aware reader).
--line comments and--[[ β¦ ]]long comments may contain unpaired quotes (-- don't); long-bracket strings[[ β¦ ]]/[=[ β¦ ]=]are verbatim bodies β a lone]inside one is safe, and their square-bracket delimiters do not count toward the block's[/]depth. Ordinary"β¦"/'β¦'strings (backslash escapes) anda[i]indexing behave as expected. Residual limit: a bare unbalanced]outside any string/comment closes the block early. - VM parity: full.
[lua | β¦ ]and[lua/eval | β¦ ]compile toOpLangBlockand run identically under--vm.
C specifics.
[clang | β¦ ]compiles the body with the system C compiler βcconPATH(macOS:xcode-select --install), overridable withAXIOMA_CC(e.g.gcc-14) β and runs the produced binary. The tag isclang, neverc(the one-letter ban; it reads "C language" the waygolangreads "Go language" β any C compiler works, not just LLVM's).- The mode slot selects the C STANDARD, not an
exec/eval split:
[clang/c99 | β¦ ]βcc -std=c99; alsoc89/c90/c11/c17/c23and thegnu89/gnu99/gnu11/gnu17/gnu23variants; no mode = the compiler's default. Unknown modes are rejected without spawning the compiler. - Body shapes: a body that defines
main()compiles verbatim (the shape C books print). Bare statements get a skeleton β standard includes (stdio.h,stdlib.h,string.h,math.h) plusint main(void) { β¦ return 0; }β so[clang | printf("%d\n", 2 + 2); ]just works, and<math.h>functions link (-lmis passed where needed). A fragment that opens with preprocessor directives but defines nomainis rejected with guidance (wrapping#includeinsidemainwould be invalid C). - Compile cache: a successful compile is cached keyed
on (source, mode, compiler identity) β identical blocks skip
ccand only re-run (~2 s cold β ~15 ms warm for a five-block script). Failures are never cached. Disable withAXIOMA_CC_NOCACHE=1; clear by deleting~/Library/Caches/axioma/clang. Runs are bounded byAXIOMA_CC_TIMEOUTseconds (default 30) with stdin at EOF, soscanf-driven book programs need their inputs replaced with constants. - Body-capture caveats (C-aware reader).
'β¦'character literals (']','\'') and////* β¦ */comments may contain unpaired quotes and brackets β the reader skips them as C does, anda[i]indexing balances the block's[/]depth naturally. Residual limit: a bare unbalanced]outside any string/char/comment closes the block early. - Errors: compile failures return catchable Errors carrying the compiler's diagnostics (plus a line-offset note when the body was auto-wrapped); a nonzero exit status is a catchable runtime Error.
- VM parity: full β verified byte-identical under
--vm.
28.3
[monkey | β¦ ] β a whole language, in-process
Every runner in Β§28.2 needs something installed. Monkey does not: the
language is implemented inside Axioma (monkey/ β lexer,
Pratt parser, tree-walking evaluator, six builtins), so the block spawns
nothing, has no dependency to miss, and runs in the browser
playground.
[monkey |
let people = ["ann", "bob", "cid"];
let i = 1;
puts(people[i]);
] # "bob\n" β Monkey arrays are 0-based
[monkey/eval | 6 * 7 ] + 8 # 50 β a typed value, composed natively
| Form | Returns |
|---|---|
[monkey | β¦ ] |
everything puts wrote, as a String |
[monkey/eval | β¦ ] |
the program's final value, typed |
Any other mode is rejected before the body runs.
The block is hermetic. Monkey cannot read an Axioma
binding and Axioma cannot read a Monkey one. This is the opposite of the
Python block's scope sharing (Β§28.1), and deliberately so: Monkey has a
single integer type, its own truthiness where 0 is
true, and 0-based indexing. A shared scope would silently
reinterpret values as they crossed. Values cross at the block's edges
instead, where the conversion is explicit β and where a value that
cannot cross faithfully is refused by name rather than approximated. A
Monkey function cannot come out (call it inside and return its result);
a hash whose 4 and "4" keys would collapse
onto one Axioma dictionary key is rejected rather than losing a
value.
It is Monkey, including the surprising parts.
7 / 2 is 3; an out-of-range index reads as
null; an if with no else is
null-valued; push returns a new array;
"a" == "a" reports
unknown operator: STRING == STRING, because Monkey has
exactly one string operator. There is no comment syntax. Three things
differ from the language as specified, each turning a host crash or an
unwritable program into an error value: string escapes are lexed,
argument count is checked, and recursion is capped (10,000 frames) with
division by zero caught.
- Errors: parse errors (all of them, with
body-relative line numbers), Monkey runtime errors, and a bad mode all
arrive as catchable Axioma
Errors. - VM parity: full β the VM calls the same entry point the tree-walker does, so the two cannot diverge.
[monkey | β¦ ] replaced a --monkey CLI flag
that was never an interpreter: it rewrote source with line-based
regexes, translating only literal array indices, so
people[i] above printed "ann" and exited 0.
Running the language is the only honest way to run the language.
28.4
Cross-language translation β [A -> B | β¦ ] /
[A --> B | β¦ ]
Where [<lang> | body] executes foreign
code, the arrow forms translate code between
languages. Two new operators slot in next to the existing pipe so the
surface stays uniform:
| Form | Semantics | Returns |
|---|---|---|
[L | body] |
execute body in L (existing) | foreign runtime value (String for exec, typed for /eval) |
[A -> B | body] |
translate body from A to B | String of B's source |
[A --> B | body] |
translate, then execute in B | typed value from B's runtime |
[nl -> B | description] |
symbolize natural-language description into B | String of B's source |
[nl --> B | description] |
symbolize, then execute in B | typed value from B's runtime |
Every form reads the body raw via the same bracket-counting reader the execute form uses β multi-line bodies, embedded brackets, and arbitrary indentation work without quoting. Body language is named on the left of the arrow, so the lexer doesn't have to disambiguate.
# Pure execution β body is Python, runs in Python (existing)
[python | print("hi")]
# Pure translation β body is Axioma, get Python source as a String
src: [axioma -> python |
pi: 3.141592653589793
area: func(r) [pi * r * r]
primes: [n | n <- range(2, 50), all([n % k != 0 | k <- range(2, n)])]
]
println(src)
# pi = 3.141592653589793
# def area(r):
# return pi * r * r
# primes = [n for n in range(2, 50) if all(n % k != 0 for k in range(2, n))]
# Translate + execute β body is Axioma, run as Python, typed value back
sum_sq: [axioma --> python | sum([n*n | n <- range(1, 11)])]
# β 385 (Python computed; marshaled to Axioma Integer)
# Reverse β body is Python source, get Axioma equivalent as a String
ax_src: [python -> axioma |
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
]
# ax_src: "fibonacci: func(n) [ if n < 2 then [return n]; fibonacci(n-1) + fibonacci(n-2) ]"
# Reverse + execute β pull Python code into Axioma scope
[python --> axioma | def double(x): return x * 2]
println(double(5)) # β 10
# Cross-language to JavaScript
[axioma -> javascript | nums: [n*n | n <- range(10)]]
# Natural-language source β describe intent, get Axioma code (always LLM)
src: [nl -> axioma | double the value 7]
# src: "double: 7 * 2" (or similar β LLM output is non-deterministic)
# Symbolize + evaluate β describe intent, get the value
v: [nl --> axioma | sum of squares from 1 to 10]
# v: 385 (or [1, 4, β¦, 100] β depends on how the LLM reads "sum of")
nl source β describe an intent, get code or a
value. The nl (alias english,
natural) source language treats the body as a
natural-language description, not source code. The LLM is
invoked with a symbolize-style prompt and returns idiomatic
target-language code. Always LLM-required (no deterministic path exists
for natural-language input), and the announce-print billing line always
fires. The --> variant Eval's the LLM output in scope,
which is powerful but inherits the LLM's non-determinism β if the model
emits syntactically-foreign tokens (like JavaScript's strict-equality
=== from its training data), the resulting parse error
surfaces with the translated source attached for debuggability.
Operator mnemonic:
- Zero arrows (
|): pure execution in the named language. - One arrow (
->): pure translation β returns a String of target source, no execution, no side effects. - Two arrows (
-->): translate + execute. ForB == "axioma"the translated source is parsed andEval'd in the current environment, so definitions in the body leak into the surrounding scope (this is what makes[python --> axioma | def double(x): β¦]followed bydouble(5)work). For foreign B, the translated source runs through the FFI in/evalmode and the typed value comes back.
Composable / String-input form: the translate()
builtin. When the source code lives in a String variable rather
than inline (read from a file, pulled from an API, etc.), use the
builtin counterpart:
py_src: read_file("script.py")
ax_src: translate(py_src, "python", "axioma") # (code, source, target)
py: translate([n*n | n <- range(10)]) # defaults: axioma β python
# β "[n * n for n in range(10)]"
Argument order is
translate(code, source_lang, target_lang) β English "from X
to Y" order, matches the legacy LLM-only translate builtin. Defaults are
source="axioma", target="python". When the
first argument is an Axioma expression (an AST node, not a String), the
AST is passed alongside so the deterministic emitter takes the fast
path.
Engine dispatch β deterministic emitter then LLM fallback
The translation engine prefers a deterministic AST emitter when it can:
- Axioma β Python has a built-in visitor that handles the common learner constructs (binding statement, function, lambda, infix arithmetic, comparison, conditional, list literal, list comprehension, call, return, identifiers, literals). Output is byte-for-byte deterministic and runs entirely offline β no LLM, no network, no API key.
- Reverse direction (any foreign source β Axioma) and Axioma β any non-Python target route through the LLM (see provider table below). Output is idiomatic but non-deterministic and requires an API key.
- Unmapped constructs (Axioma β Python forms outside the deterministic subset β relational rules, modal logic, MVL values, etc.) fall back to the LLM automatically.
The deterministic emitter is the reason
[axioma -> python | [n*n | n <- range(10)]] works
without configuring any provider β and why the deterministic forward
direction is exercised in
tests/axioma/translation/test_translate_builtin.ax (35
byte-for-byte assertions).
LLM providers and billing transparency
When the LLM path is taken, every call prints one line to stderr before the network request goes out, naming the provider, model, endpoint, and whether it's a paid API. The line is meant to prevent "surprise on the credit card":
[axioma translate] python β axioma provider=openrouter model=google/gemini-2.5-flash-lite endpoint=https://openrouter.ai/api/v1 (paid API)
Local Ollama gets (local, no billing). The print is on
stderr so it doesn't pollute the translated source returned to scripts.
Suppress with AXIOMA_TRANSLATE_QUIET=1 once you've
confirmed your provider choice.
Provider auto-selection walks the following priority, picking the first one whose API key is set in the environment:
| Provider | Env var(s) | Default model | Endpoint | Notes |
|---|---|---|---|---|
| OpenRouter | OPENROUTER_API_KEY_AXIOMALANG (project-scoped) or
OPENROUTER_API_KEY |
google/gemini-2.5-flash-lite |
openrouter.ai/api/v1 |
Routes to many backends; project-scoped key takes precedence |
| Anthropic | ANTHROPIC_API_KEY |
claude-3-opus-20240229 |
api.anthropic.com/v1 |
Claude family |
| Gemini | GEMINI_API_KEY |
gemini-2.5-flash |
generativelanguage.googleapis.com/v1beta |
Google direct |
| Grok (xAI) | XAI_API_KEY or GROK_API_KEY |
grok-4 |
api.x.ai/v1 |
Distinct from Groq |
| OpenAI | OPENAI_API_KEY |
gpt-4o-mini |
api.openai.com/v1 |
|
| Groq | GROQ_API_KEY |
llama-3.1-70b-versatile |
api.groq.com/openai/v1 |
Inference vendor, not xAI |
| Ollama | none (local) | llama2 |
localhost:11434 |
Override via OLLAMA_MODEL=β¦ |
OpenRouter is first because it routes to many backend models behind
one billing surface β the recommended default. Override the OpenRouter
model with OPENROUTER_MODEL=anthropic/claude-3.5-sonnet (or
any other OpenRouter slug) without recompiling.
Multi-line bodies and
"""β¦""" are unnecessary
A common first instinct is to wrap foreign source in a triple-quoted string for the reverse direction:
# DON'T β body is read raw, no string quoting needed:
[axioma <- python | """
def f(x):
return x * 2
"""]
# DO β bracket-counting raw reader handles multi-line directly:
[python -> axioma |
def f(x):
return x * 2
]
The raw reader closes on the matching outer ] and counts
inner brackets correctly, so [1, 2, 3] and nested
comprehensions inside the body work as long as their brackets
balance.
MCP integration
The same translation engine is exposed via the
translate_code tool on the MCP server
(axioma --mcp). Clients call it with code,
source_lang, target_lang and get back the same
engine output β deterministic when applicable, LLM-backed otherwise β
plus the parsed AST as JSON when the source is Axioma. One engine, two
surfaces.
28.5 The
[sql | β¦ ] block β embedded SQL
Where [python | β¦] calls out to a foreign runtime,
[sql | β¦] compiles SQL down to native
Axioma. The block is an expression; its value is the result of running
the compiled comprehension/ transaction. There is no foreign process, no
marshaling β SQL is treated as another surface over Axioma's
relational substrate, sitting beside the pipe-form and Prolog-form
comprehensions covered in Chapter 7.
[sql | CREATE TABLE teacher (instructor VARCHAR, student VARCHAR)]
[sql | INSERT INTO teacher VALUES ('Socrates', 'Plato')]
[sql | INSERT INTO teacher VALUES ('Plato', 'Aristotle')]
[sql | SELECT student FROM teacher WHERE instructor = 'Socrates']
# β {"Plato"}
The same data is reachable via Axioma's native idioms:
# Set comprehension
{Y | teacher("Socrates", Y)}
# β {"Plato"}
# Prolog-form
{Y | Y <- teacher("Socrates", Y)}
# β {"Plato"}
All three surfaces produce identical results because they all lower
to the same comprehension over the _relation_ store.
DDL β
CREATE TABLE, DROP TABLE,
TRUNCATE
# CREATE TABLE β declares a new relation
[sql | CREATE TABLE emp (name VARCHAR, dept_id INTEGER, salary INTEGER)]
# Parameterized types: VARCHAR(N), DECIMAL(P, S), etc. The size
# arguments are accepted at parse time for SQL compatibility but
# discarded at emission β Axioma's relations are positionally typed,
# not nominally, so size caps are not enforced.
[sql | CREATE TABLE prices (sku VARCHAR(32), amount DECIMAL(10, 2))]
# IF NOT EXISTS β idempotent CREATE; Axioma's `relation X(...)` is
# already idempotent, so this is an emission no-op accepted for SQL
# surface compatibility.
[sql | CREATE TABLE IF NOT EXISTS emp (name VARCHAR, dept_id INTEGER)]
# DROP TABLE β removes the relation's schema, all stored facts at
# every grounding tier, and parallel metadata in one sweep.
[sql | DROP TABLE emp]
# IF EXISTS β silent no-op if missing (drop_relation is already a
# silent no-op for unknown relations).
[sql | DROP TABLE IF EXISTS doesnt_exist]
# TRUNCATE TABLE β clear the extent, preserve the schema. Faster
# than DELETE without a WHERE because no per-row predicate runs.
# The TABLE keyword is optional (Postgres-style).
[sql | TRUNCATE TABLE emp]
[sql | TRUNCATE emp] # equivalent
DML β INSERT,
UPDATE, DELETE
# INSERT β¦ VALUES
[sql | INSERT INTO emp VALUES ('Alice', 1, 90000)]
# INSERT β¦ SELECT (copy rows from another relation)
[sql | INSERT INTO emp_backup SELECT * FROM emp WHERE dept_id = 1]
# UPDATE with arithmetic RHS
[sql | UPDATE emp SET salary = salary * 1.10 WHERE dept_id = 1]
# DELETE
[sql | DELETE FROM emp WHERE salary < 50000]
All three DML statements wrap the read+write in a transaction so a partial failure rolls back cleanly.
Queries β
SELECT with the full join family
# Single-table SELECT with WHERE
[sql | SELECT name, salary FROM emp WHERE dept_id = 1]
# DISTINCT
[sql | SELECT DISTINCT dept_id FROM emp]
# INNER JOIN
[sql | SELECT emp.name, dept.dname FROM emp
INNER JOIN dept ON emp.dept_id = dept.id]
# LEFT [OUTER] JOIN β preserves unmatched left rows, NULL-pads right
[sql | SELECT emp.name, dept.dname FROM emp
LEFT JOIN dept ON emp.dept_id = dept.id]
# RIGHT [OUTER] JOIN β preserves unmatched right rows
[sql | SELECT emp.name, dept.dname FROM emp
RIGHT OUTER JOIN dept ON emp.dept_id = dept.id]
# FULL [OUTER] JOIN β preserves both
[sql | SELECT emp.name, dept.dname FROM emp
FULL OUTER JOIN dept ON emp.dept_id = dept.id]
# Comma-FROM (pre-SQL-92 implicit cross-join β the constraints
# come from WHERE)
[sql | SELECT emp.name, dept.dname FROM emp, dept
WHERE emp.dept_id = dept.id]
# Three-table comma-FROM
[sql | SELECT emp.name, dept.dname, region.rname
FROM emp, dept, region
WHERE emp.dept_id = dept.id AND emp.dept_id = region.id]
NULL padding for OUTER joins follows the active refinement
(/b4 default β belnap("neither"), displayed
?α΅; /k3 β kleene("unknown")).
Aggregates β
GROUP BY, HAVING,
COUNT/SUM/AVG/MIN/MAX
# Single-column GROUP BY with COUNT
[sql | SELECT dept_id, COUNT(*) FROM emp GROUP BY dept_id]
# SUM with WHERE
[sql | SELECT dept_id, SUM(salary) FROM emp
WHERE salary > 50000 GROUP BY dept_id]
# Multi-column GROUP BY β each distinct (col1, col2, ...) tuple
# gets its own group; result rows flatten to (col1, col2, ..., agg)
[sql | SELECT region, product, SUM(qty) FROM sales
GROUP BY region, product]
# HAVING β filter post-aggregation
[sql | SELECT dept_id, SUM(salary) FROM emp
GROUP BY dept_id HAVING SUM(salary) > 200000]
# Single-column scalar aggregate (no GROUP BY)
[sql | SELECT COUNT(*) FROM emp WHERE dept_id = 1]
# β 3
Phase 5 caveat: single aggregate per SELECT, and HAVING currently requires single-column GROUP BY.
Expressions β
CAST, CASE, LIKE,
BETWEEN, IN, EXISTS
# CAST β type conversion (CAST(expr AS type) and Postgres :: shorthand)
[sql | SELECT CAST(score AS FLOAT) FROM grades WHERE student = 'Alice']
[sql | SELECT score :: FLOAT FROM grades WHERE student = 'Alice']
# Chained postfix casts
[sql | SELECT score :: INT :: TEXT FROM grades]
# Searched CASE β independent predicates per branch
[sql | SELECT name,
CASE WHEN salary > 100000 THEN 'high'
WHEN salary > 50000 THEN 'mid'
ELSE 'low'
END
FROM emp]
# Simple CASE β subject compared by equality
[sql | SELECT name,
CASE grade WHEN 'A' THEN 'excellent'
WHEN 'B' THEN 'good'
ELSE 'fair'
END
FROM grades]
# LIKE β SQL wildcards (% any-sequence, _ single-char)
[sql | SELECT name FROM emp WHERE name LIKE 'A%']
# BETWEEN / NOT BETWEEN β inclusive range
[sql | SELECT name, salary FROM emp WHERE salary BETWEEN 60000 AND 90000]
# IN (literal list)
[sql | SELECT name FROM emp WHERE dept_id IN (1, 2, 3)]
# IN (SELECT β¦) subquery
[sql | SELECT name FROM emp
WHERE dept_id IN (SELECT id FROM dept WHERE dname = 'Eng')]
# EXISTS β non-empty subquery test
[sql | SELECT name FROM emp e WHERE EXISTS
(SELECT 1 FROM dept d WHERE d.id = e.dept_id)]
Set operations β
UNION, INTERSECT, EXCEPT
# UNION β set union (deduplicates)
[sql | SELECT name FROM emp_jan
UNION
SELECT name FROM emp_feb]
# UNION ALL β bag union (preserves duplicates)
[sql | SELECT name FROM emp_jan
UNION ALL
SELECT name FROM emp_feb]
# INTERSECT β set intersection
[sql | SELECT name FROM emp_jan INTERSECT SELECT name FROM emp_feb]
# EXCEPT β set difference
[sql | SELECT name FROM emp_jan EXCEPT SELECT name FROM emp_feb]
Common Table Expressions β
WITH
# Single CTE
[sql | WITH high_paid(name, salary) AS
(SELECT name, salary FROM emp WHERE salary > 100000)
SELECT name FROM high_paid ORDER BY salary DESC]
# Chained CTEs β each later CTE can reference earlier ones
[sql | WITH
eng(name, salary) AS (SELECT name, salary FROM emp
WHERE dept_id = 1),
high(name, salary) AS (SELECT name, salary FROM eng
WHERE salary > 100000),
greeted(greet) AS (SELECT 'Hi ' + name FROM high)
SELECT greet FROM greeted]
Refinement
modes β /bag, /k3, /strict,
/distinct, /explain
The block accepts SQL-shaping refinements that pre-configure how the compiled comprehension treats duplicates, NULLs, and shape strictness:
| Refinement | Effect |
|---|---|
[sql/bag | β¦] |
Duplicate-preserving (bag) semantics |
[sql/distinct | β¦] |
Force DISTINCT projection (default) |
[sql/k3 | β¦] |
SQL NULL lowers to Kleene K3 unknown |
[sql/strict | β¦] |
Strict shape checking on projection arity |
[sql/explain | β¦] |
Return the compiled Axioma source as a String β useful for teaching and debugging |
src: [sql/explain | SELECT name FROM emp WHERE dept_id = 1]
println(src)
# {V1 | emp(V1, 1, V3)}
Lineage surface
β [sql -> algebra] /
[sql -> calculus]
For pedagogical traceability there are two "translation" forms that emit the intermediate-representation equivalents of the SQL:
[sql -> algebra | SELECT name FROM emp WHERE dept_id = 1]
# β "Ο_{name}(Ο_{dept_id = 1}(emp))"
[sql -> calculus | SELECT name FROM emp WHERE dept_id = 1]
# β "{ t.name | t β emp β§ t.dept_id = 1 }"
These don't run the query β they're string outputs of the relational- algebra and tuple-calculus forms, useful for the textbook chapter on database theory.
Identifying what's not yet covered
Phase 5 explicitly defers a few large features to follow-on landings:
window functions (OVER,
PARTITION BY, ROW_NUMBER), correlated
subqueries (subqueries that reference outer-query columns),
CREATE TABLE constraints
(PRIMARY KEY, FOREIGN KEY,
NOT NULL, DEFAULT, REFERENCES,
CHECK), ALTER TABLE,
multiple aggregates per SELECT, chained outer
joins, non-equijoin OUTER JOIN predicates, and
HAVING with multi- column GROUP BY. None of them
prevent the working examples above from running.
27.4b Three quietly important language fixes
Three small changes to the core language that came out of the comparison-tooling work and benefit every Axioma program, not just Python interop.
Short-circuit and / or.
Previously both operands were evaluated even when the first determined
the result. The natural guard pattern
if i <= len(a) and a[i] > 0 then β¦
errored with "index out of bounds" because a[i] ran when
i was out of range. Now and and
or short-circuit Boolean operands the way every other
modern language does: the right side is only evaluated when the left
doesn't already settle the question. The multi-valued logics (Belnap,
Εukasiewicz, etc.) are unaffected β they still need both inputs for
their lattice operations.
[] in then / else is
an empty array. Previously parsed as an empty block (which
evaluates to null), silently breaking natural recursive
base cases:
to_list: func(t) [
if t == none then [] else [t.value] + to_list(t.tail)
]
Pre-fix this stack-overflowed because the base case returned
null and null + [x] fails type-check (which
the recursion never gets to anyway because the recursion never bottoms
out on a null it keeps trying to concatenate). Same fix
simultaneously enables then [x] + ys and other forms where
the bracketed clause is followed by an infix operator β both now extend
correctly past the closing bracket.
for as alias for foreach.
Python/JS/Java/Rust developers expect for x in xs [body].
Axioma already had foreach x in xs [body] doing exactly
this; for is now a lexer-level alias that produces the same
AST. Both forms are first-class and interchangeable β pick whichever
reads better. Destructuring works under both names:
for [k, v] in pairs [...].
py.eval / py.exec
auto-capture. The [python | β¦] block form already
auto-injected free Axioma identifiers into the Python scope. The
shim-style py.eval(string) did not β the string shipped
verbatim. Now the two paths agree:
xs: [1, 2, 3, 4, 5]
py.eval("sum(xs)") # 15 β xs is auto-captured
py.exec("print(len(xs))") # 5
For multi-statement bodies under py.eval, the runtime
lifts the prelude through exec + json.dumps
internally so the typed-result contract is preserved.
27.5a Comparison-friendly tools
Three small additions make side-by-side Axioma/Python work feel natural rather than ad-hoc.
bench(label, fn) β
measure one call
xs: [3, 1, 4, 1, 5, 9, 2, 6]
ax: bench("axioma sort", func() [my_sort(xs)])
py: bench("python sort", func() [py.call("sorted", xs)])
println(ax.label, ax.elapsed_ms, "ms vs", py.label, py.elapsed_ms, "ms")
println("agree?", ax.result == py.result)
bench returns an ObjectMap with three keys:
label, elapsed_ms (Float, sub-millisecond
precision), and result. Use it whenever you want to compare
two implementations on more than just behavior.
:compare
(REPL) β typed-value diff with timing
:compare 2 + 3 // 2 + 3
axioma 5 [233Β΅s]
python 5 [368Β΅s]
β values agree
:compare [1, 2, 3] // [3, 2, 1]
axioma [1, 2, 3] [17Β΅s]
python [3, 2, 1] [54Β΅s]
β values differ
Both sides are evaluated as typed expressions (the
Python side goes through py.eval, so it returns Axioma
typed values, not captured stdout). Equality is structural β arrays and
object-maps compare element-wise; integers and floats coerce freely.
lib/cs/data_structures.ax
β pure-Axioma classics
A small library of canonical data structures so you have something real to compare against rather than building each from scratch:
| Structure | Constructor | Key operations |
|---|---|---|
| LinkedList | list_create() |
list_push, list_pop,
list_size, list_from_array,
list_to_array |
| Binary Search Tree | bst_create() |
bst_insert, bst_contains,
bst_inorder, bst_size,
bst_from_array |
| Stack | stack_create() |
stack_push, stack_pop,
stack_peek, stack_size |
| MinHeap | heap_create() |
heap_push, heap_pop,
heap_peek, heap_size,
heap_from_array, heap_drain_sorted |
Style: functional/persistent for the recursive structures
(LinkedList, BST), array-backed with mutation for the index-heavy ones
(Stack, MinHeap). Each instance is an ObjectMap; persistent
operations take the instance and return an updated copy.
import "lib/cs/data_structures.ax"
h: heap_from_array([3, 1, 4, 1, 5, 9, 2, 6])
ax_sorted: heap_drain_sorted(h)
py_sorted: [python/eval | sorted([3, 1, 4, 1, 5, 9, 2, 6])]
println("agree?", ax_sorted == py_sorted)
HashMap is intentionally absent β Axioma's native
ObjectMap is already a hash table and reads as one
(m.key, m["key"], len(m.keys)).
Wrapping it in a hashmap_* API would just add ceremony.
27.5 Walkthrough: lists and loops, three ways
Same algorithm, three styles. The point is not that any one is best β it's that mixing is cheap, so you can adopt incrementally.
Goal: given a list of numbers, compute the mean and a rough standard deviation, then print the values that are more than one Ο above the mean.
Style 1 β pure Axioma
xs: [4, 8, 15, 16, 23, 42]
n: len(xs)
mean: sum(xs) / n
var: sum([(x - mean) * (x - mean) | x <- xs]) / n
sigma: math.sqrt(var)
outliers: [x | x <- xs, x > mean + sigma]
println("mean=", mean, "sigma=", sigma, "outliers=", outliers)
math.sqrt is the NATIVE ambient math package (Β§27.2) β
no import, no foreign runtime. Spelling it python.math.sqrt
would run CPython's instead; the call site is what says which.
Style 2 β inline Python block
xs: [4, 8, 15, 16, 23, 42]
report: [python |
import statistics
m = statistics.mean(xs)
s = statistics.stdev(xs)
outliers = [x for x in xs if x > m + s]
print(f"mean={m} sigma={s} outliers={outliers}")
]
println(report)
The Axioma xs is auto-captured into the Python scope.
The whole Python body runs as one subprocess (or one round-trip in the
persistent worker), and the captured print(...) text comes
back as an Axioma String.
Style 3 β typed-result block
(/eval)
xs: [4, 8, 15, 16, 23, 42]
stats: [python/eval |
{"mean": __import__('statistics').mean(xs),
"sigma": __import__('statistics').stdev(xs)}
]
outliers: [x | x <- xs, x > stats.mean + stats.sigma]
println("from python:", stats, " outliers:", outliers)
Now the Python side returns a typed ObjectMap, and the
outliers filter is back in Axioma where it composes with the rest of the
program. This is usually the right balance: borrow Python's library for
the hard part, keep the surrounding logic native.
Comparing two styles in the REPL
:compare math.sqrt(2) // statistics.stdev([1,2,3,4,5])
Both halves run, and the REPL prints them side-by-side with a β when
they match. Useful when porting a Python snippet β write the Axioma
version, paste the original on the right of //, watch them
agree (or not).
Picking the style
| Situation | Style |
|---|---|
| The library exists in Axioma | Pure Axioma |
| You need a Python stdlib function for one expression | Shim (e.g. math.sqrt,
statistics.mean) |
| Multi-line Python you don't want to translate yet | `[python |
| You want a typed value back from Python | `[python/eval |
| You want to see the Python equivalent of your Axioma | `[axioma -> python |
| You want Axioma computed via Python's runtime | `[axioma --> python |
| You're learning from a Python snippet | `[python -> axioma |
| Pull Python code into Axioma scope | `[python --> axioma |
| Code comes from a String variable | translate(src, "python", "axioma") builtin |
| You know what you want but not the Axioma syntax | `[nl -> axioma |
| You want the value but don't care about the code | `[nl --> axioma |
| You're porting and want a safety net | :compare while you write the Axioma version |
Performance notes:
- The persistent worker is on by default
(lazy-spawned on first Python use, so it costs nothing if no
[python | β¦]block runs). Same script reuses one long-lived subprocess at ~0.5β2ms per call. - Pass
--no-python-workerto disable. Each block then spawns its own freshpython3 -cprocess (~30-50 ms). Use this when you need hard isolation between unrelated blocks or when running in a restricted environment that disallows long-lived subprocesses.
Globals persist across blocks (worker mode)
This is the most important behavior to know about the default. With the worker on:
_setup: [python | x = 41 ]
r: [python/eval | x + 1 ] # β 42 β x carries over
The two blocks share Python's namespace. That matches REPL semantics and is what you want most of the time. But if you paste two unrelated snippets into the same script and they happen to use the same variable names, they'll see each other.
Three options when you need isolation:
py.reset()β clears worker globals between blocks without leaving worker mode:r1: [python | x = "first" ] py.reset() r2: [python | print('x' in dir()) ] # False- Use a unique prefix for variables in self-contained snippets, or wrap the snippet in a function so its locals don't escape.
--no-python-workerβ fall back to per-call subprocess for the whole run. Slower, but every block starts from a fresh namespace.
27.6 Narrowing the boundary (worker-mode features)
The persistent Python worker is on by default. Four extra channels
become available that make the boundary feel less like a foreign-
function call and more like a peer (none of these work with
--no-python-worker β the per-call subprocess can't talk
back over stdio):
Unified namespaces
python.eval("math.sqrt(2)") # typed Float (py.β¦ = exact alias)
python.exec("print('hi')") # captured stdout (String)
python.call("math.sqrt", 2) # function-style, typed result
python.import_("numpy") # make numpy available downstream
python.reset() # clear worker globals
julia.exec("println(2+3)") # same shape for julia / rlang / js
The same python module carries the stdlib shim
sub-modules (python.math, python.statistics, β¦
β Β§27.2), so "how do I call Python" has one answer whatever the
granularity.
One obvious entry point per dialect, instead of "block exec / block
eval / shim β pick the right one." All four dialects (py,
julia, r, js) expose
.exec; py additionally exposes
.eval, .call, .import_, and
.reset (clear globals between blocks).
Reverse calls β Python invokes Axioma
double: func(x) [x * 2]
format: func(x, y) ["x=" + x + ", y=" + y]
[python | print(axioma.call("double", 21)) ] # 42
[python | print(axioma.call("format", 10, 20)) ] # x=10, y=20
Inside any Python block, axioma.call(name, *args)
resolves name in the surrounding Axioma scope, marshals
args, calls the function, and returns the typed result. Python errors
and Axioma errors propagate across the boundary as
RuntimeError.
Object handles β large values, no deep copy
For values too big to want to marshal whole, the
axioma.* namespace exposes lazy access:
big: [...] # imagine a 1M-element array
[python/eval | axioma.len("big")] # length only, no copy
[python/eval | axioma.at("big", 0)] # single element
[python/eval | axioma.array("big")[100:110]] # slice via proxy
axioma.array(name) returns a Python proxy whose
__len__, __getitem__, and
__iter__ round-trip per access. The underlying Axioma array
is never deep-copied; you pay a per-element fetch cost but avoid the
upfront marshalling time and the memory pressure.
Streaming progress β
axioma.emit
Long-running blocks can report intermediate values:
[python |
import time
for i in range(5):
axioma.emit(f"step {i+1}/5")
time.sleep(1.0)
print("done")
]
Each axioma.emit(value) writes a one-way stream message
that the Axioma side prints live (default formatter prefixes with
[emit]). The block's own return value is unaffected β it's
still the captured stdout (or, for /eval, the typed
expression result). Stream and return are independent channels.
What's still missing
- One-way scope only. Axioma values flow into Python; mutations inside
Python don't propagate back. Use the block's return or
axioma.callto write a setter explicitly. - Reverse calls require worker mode. Per-call subprocess
(
python3 -c) can't talk back over stdio. - Eval mode requires single-expression bodies. Multi-statement bodies
with auto-capture get lifted through exec internally; bare
multi-statement /eval errors with
SyntaxError.
Disambiguation rule: [expr | var <- iterable, β¦] is
still a list comprehension. The lang-block form is taken only when the
first token is a registered dialect identifier and the next token is
|.
27.2 Ambient
packages and the python.* namespace
Two namespace layers work with no import ceremony, and one prefix rule tells them apart (the namespace-resolution model, July 2026):
- a bare package name is always NATIVE β
math.sqrtis the Go implementation, never a foreign runtime; - a language-prefixed name is explicitly FFI β
python.math.sqrtruns CPython'smath.sqrtthrough the Python FFI. The prefix makes the cost model visible:python.β¦crosses a process boundary, the bare name does not.
Ambient native packages. The builtin packages
math, datetime, io,
logger, and os are seeded into every session
as modules β Lua-stdlib ergonomics:
math.sqrt(2) # 1.4142135623730951 β native, no import
math.pi # (also e / tau / inf, and the classic PI / TAU)
math.gcd(48, 18) # 6 (factorial is big-integer exact)
datetime.now() # DateTime β same package Β§4 documents
io.read_file("notes.txt") # the file layer
os.getenv("HOME") # the process layer (see below)
import "builtin:math" as M still works unchanged (it
binds an alias to the same package β use it for renaming), a user
binding (math: 5) replaces the seed like any seeded name,
and bindings() does not list the seeds. Under
--vm the modules are absent (a clean compile-time rejection
β the pre-existing module boundary). The browser playground ships
math and datetime;
io/logger/os are absent there,
the honest answer for a sandboxed runtime. string and
array remain import-only for now.
The os package closes the classic
process-layer gaps (os.date itself lives in
datetime β strftime is
directive-compatible):
| Member | Returns |
|---|---|
os.getenv(name [, default]) |
String, the default, or none |
os.environ() |
Dictionary snapshot of the process env |
os.clock() / os.monotonic() |
Float CPU seconds / wall seconds since start |
os.hostname() / os.platform() /
os.pid() |
host name / "darwin/arm64" / Integer |
os.args() |
Array of args after the script path |
os.tmpdir() |
String |
os.run(cmd [, timeout_secs]) |
{stdout, stderr, code} Dictionary |
os.exit([code]) |
terminates the process |
os.run is the structured shell runner: a nonzero exit is
data (r.code), not an error; only failure
to start, or the timeout (per-call arg,
AXIOMA_OS_RUN_TIMEOUT, default 60 s), is a catchable Error.
Only the single trailing newline of
stdout/stderr is trimmed.
The python.* umbrella (the relocated
shims). The curated Python-stdlib shims β thin bindings that
marshal their arguments to Python literals and round-trip through the
python_interop FFI β live as sub-modules of the ambient
python namespace (py is an exact alias, and
the call surface python.eval / exec /
call / import_ / reset lives on
the same module):
python.math.sqrt(2) # CPython's math.sqrt, explicitly
python.statistics.mean([1,2,3]) # 2
python.json.dumps([1,2,3]) # "[1, 2, 3]"
python.regex.findall("\d+", "a 12 b 345")
python.itertools.combinations([1,2,3,4], 2)
python.collections.Counter("aabbb")
python.eval("2**10") # 1024 β the general escape hatch
Before July 2026 the shims occupied the BARE names
(math.sqrt was a Python subprocess), which blocked
the native math package from the obvious name. The
relocation is loud, not silent: a leftover bare spelling
(statistics.mean(xs)) errors with a static pointer at the
python.* form β the interpreter never probes a foreign
runtime on an error path. The python.math constants
(pi/e/tau/inf) are
seeded from Go's bit-identical IEEE doubles, so startup spawns no Python
(and python.math.inf exists at all β the old JSON
round-trip silently dropped it).
(The random.* shim module was deleted
in July 2026 when the native random /
random_seed / shuffle / sample
family shipped β see Β§22 "Randomness". Native spellings:
random.randint(a, b) β random(a, b),
random.choice(xs) β sample(xs),
random.seed(n) β random_seed(n).)
To skip the python/py seeding (stricter
scripts, or when Python isn't on PATH β the native ambient
packages stay either way):
axioma --no-shims script.ax
27.3 REPL affordances
:hints on # surface translation tips inline as you type
:hints status # show on/off + catalog size
:hints list # print the full hint catalog
:hint range # one-shot lookup
:compare sum([1,2,3]) // sum([1,2,3])
# run the Axioma side and the Python side, compare
:compare is the side-by-side tool: type an Axioma
expression on the left of // and a Python expression on the
right; both run against the live REPL environment and a β/β flag tells
you whether they produced identical printed values.
27.4 When to use what
| Use | Tool |
|---|---|
| Quick port of a Python snippet you already trust | `[python |
| Calling a single native helper inline | math.sqrt(x) (ambient package) |
| Calling a Python-stdlib helper explicitly | python.statistics.mean(xs) |
| Tutorial / onboarding ("look how similar this is") | :compare |
| Catching pasted Python in a fresh REPL | :hints on |
The tests under tests/axioma/lang_blocks/ exercise both
the block form and the shim catalog.
29. Errors as First-Class Values
Errors in Axioma are catchable, inspectable values,
not just halts β a value model with no stack-unwinding exception.
Failure stays distinct from error: an empty query
result or none is not an error; only a genuine fault
(division by zero, undefined word, type mismatch, β¦) produces an
Error value.
Four failure policies, one expression
The same expression can be wrapped four ways, depending on what you want when it faults:
| Form | On success | On error | Reach for it when⦠|
|---|---|---|---|
EXPR |
value | halts / propagates | errors should propagate |
try EXPR |
value | the Error value (inspectable) | you want to inspect or classify it |
EXPR otherwise D |
value | D |
you have a specific fallback |
attempt EXPR |
value | none |
you just want "nothing" on failure |
trybinds tightly, like a unary operator: it grabs a single primary β a parenthesized group, a call, or member access β so a trailing operator applies to the caught result. Thustry(risky()) is Errorβ‘(try(risky())) is Errorβtrue, matching the bind-it-first idiome: try(risky()); e is Error. To make a whole binary computation optional, parenthesize it:try (a / b).attemptand theotherwiseoperator still bind their operand greedily to the end of the expression β parenthesize to scope those tighter.
try β catch to a value
e: try(10 / 0) # caught β does NOT halt; e IS the error value
error?(e) # β true
type(e) # β "Error" (e is Error β true β seeded primitive Concept)
e.message # β "division by zero"
e.hint # β recovery hint (also .kind / .line / .column / .source / .file)
try(2 + 3) # β 5 (success passes through untouched)
Constructing & re-raising
b: error("boom", "fix it") # build an inert error VALUE (not raised)
b.message # β "boom" ; b.hint β "fix it"
try(raise(b)) # raise() re-arms propagation; try catches it back
otherwise
/ or else β the fallback railway
LEFT otherwise RIGHT (also spelled
LEFT or else RIGHT) returns LEFT's value, or β
if LEFT faults β falls back to RIGHT. It is
itself a catch point, so no try wrapper is needed.
RIGHT is evaluated lazily (only on
failure), and the operator is the loosest real operator, so both sides
form fully before it combines them.
parse_int("xx") otherwise 0 # β 0 (catches the parse fault directly)
(10 / 0) otherwise 99 # β 99
lookup(k) or else "not found" # two-word spelling, identical semantics
a() otherwise b() otherwise c() # left-assoc chain: first success wins
otherwise is a soft keyword β
otherwise: 5 is still a valid binding; it reads as the
operator only at the infix slot between two same-line expressions.
attempt β swallow to
none
n: attempt parse_int(user_input) # an Integer, or `none` if it didn't parse
(attempt (1 / 0)) == none # β true
(attempt (1 / 0)) == om # β false (the dual-null distinction is load-bearing)
A failed computation has no result
(none β Frege's "no Bedeutung"), not an
undetermined one (om β SETL's Ξ©). Use
attempt for the none-result form and otherwise
for a non-none default; they are alternatives, not
partners.
Deep recursion is a catchable error
Unbounded or very deep recursion returns a clean,
catchable Error instead of crashing the
host with a stack overflow. The guard bounds Eval
re-entrancy depth, so it covers every body shape:
spin: func(n) [if n <= 0 then 0 else spin(n - 1)]
e: try(spin(100000)) # caught β interpreter stays usable afterward
e is Error # β true ("recursion limit exceeded")
recursion_limit() # β the live ceiling (50000 native; 350 under wasm)
recursion_limit() reports the current ceiling. The
default is target-specific β the browser/playground stack is far tighter
than a native goroutine stack β and the VM is already safe (it recurses
on an explicit capped frame stack, returning a clean
"stack overflow").
Error kinds as sub-Concepts
A caught error classifies into a kind, so you can
branch on why it failed. Six built-in kinds are seeded as
sub-Concepts of Error:
| Concept | Matches errors whose message says⦠|
|---|---|
DivByZero |
β¦ by zero (division / modulo / divmod / quotient /
remainder) |
TypeError |
type mismatch β¦ / unknown operator β¦ |
NameError |
Undefined word β¦ /
identifier not found |
IndexError |
index out of bounds / out of range |
ArityError |
wrong number of arguments |
CallError |
not callable / not a function |
e: try(10 / 0)
e is DivByZero # β true
e is Error # β true (the generic Error concept still matches any error)
e.kind # β "DivByZero" (agrees with the `is` test)
if e is NameError then println("typo in a name")
else if e is TypeError then println("incompatible types")
else println(e.message)
# stamp a kind authoritatively on a user error (survives raise + re-catch):
u: error("bad input", "expected a number", "TypeError")
u is TypeError # β true
The kind is derived on demand from the error's
message by a central matcher, so the hundreds of existing error sites
need no per-site tagging; unrecognized messages classify as generic
Error only, never misattributed.
VM parity:
try/otherwise/attemptare evaluator-only β under--vmthey report a cleancompilation not implemented. The value-level builtinserror()/raise/error?do work under--vm. Porting the whole errors-as-values floor to the VM is a single later phase.
30. The Cognitive Kernel
After procedural, object-oriented, functional, and logical, Axioma
adds a cognitive layer whose defining primitive is
understand. Computing as the mind does β the mind,
in one word, models; the knowledge base is
that world-model, and to understand(X) is to model X into
the model of everything. The kernel adds abduction β
Peirce's third inference mode β so deduction (strict Horn
<==), induction (defeasible <~~), and
abduction (abduce) all finally have a home.
| Builtin | Role | Returns |
|---|---|---|
abduce("rel", aβ¦) |
inference to an explanation (Peirce) | Array of
(explanation, "strict"/"defeasible") |
examine(Concept) / examine("rel", aβ¦) |
the gate (System 2): meaning Β· warrant Β· contract Β· suspicion | ObjectMap verdict |
understand(β¦) |
the engine: represent β abduce + predict β examine β graded verdict | ObjectMap |
relation bird(x)
relation flies(x)
flies(X) <~~ bird(X)
assert bird("tweety")
abduce("flies", "tweety")
# β [("bird("tweety")", "defeasible")] the rule body whose head derives it
concept Phlogiston { formed_by: "stipulation" }
examine(Phlogiston)
# β {meaningful: false, pseudo: true, contract: ?α΅,
# verdict: "pseudo-concept β an empty word (no boundary, examples, or instances)", ...}
understand("flies", "tweety")
# β {represents, examination, explanations, predicts, coherent, verdict}
All three builtins are shadowable (a user binding of
the same name wins). Each also has a natural-language
surface that self-displays, like why:
understand Phlogiston # β println(understand(Phlogiston)) (TitleCase-concept arg)
examine "gravitates" # string arg β fact mode
abduce flies("tweety") # fact-call form β the sibling of `why <conclusion>`
The NL prefixes are soft: they fire only on a literal /
TitleCase-concept argument (or, for abduce, a fact-call);
bindings (understand: β¦) and ordinary calls
(understand(x)) fall through untouched.
Rule heads auto-register as iterable relations.
H(X) :- BmakesHiterable (for y in H), queryable, and introspectable (@H == "Relation") with no separaterelation Hdeclaration β you can iterate whatever you can derive.
[ model | β¦ ] β
the advisory lifecycle lens
The authoring counterpart to the kernel.
[ model | body ] runs body as native Axioma in
the current environment and prints an advisory
panel placing your code on the epistemic lifecycle of a knowledge model
β represent β ground β infer β prove β assess-truth β
apply β naming the machinery you have not reached for yet. It
is advisory, not gating: your code runs exactly as
written, and the block returns the body's last expression.
result: [ model |
relation mortal(x)
axiom mortal("socrates") # represent + ground (as an axiom)
human(X) :- mortal(X) # infer (strict backward rule)
{X | X <- human(X)} # query β {"socrates"}
]
# panel β stderr:
# ββ model Β· epistemic lifecycle βββββββββββββββββββββββββββββββββββββ
# β β represented
# β β grounded
# β β inferred
# β β proved β why <conclusion> Β· proof(rel, argsβ¦)
# β β truth-valued β set_truth(rel, argsβ¦, "both") Β· truth(rel, argsβ¦)
# β β applied β check / examine(Concept) (β B4) Β· understand(β¦) runs the whole arc
# ββ 3/6 phases present Β· advisory only β your code runs exactly as written.
[ model/report | β¦ ]runs the body but returns the classification as a dot-accessibleObjectMap({represented, grounded, inferred, proved, truth_valued, applied, present, total, value}) instead of printing β so code can branch on the verdict (if rep.grounded then β¦).- Suppress the panel with
AXIOMA_MODEL_QUIET=1. The only accepted refinement is/report. Evaluator-only, like every language block.
31. Proof Assistant
Axioma hosts a small, auditable natural-deduction proof
checker as an importable library β lib/proof. You
write a proof as a list of numbered steps, hand it to
certify against a goal, and get back a sealed
CheckedTheorem: a value that exists only
because the proof actually checks. A bad proof never crashes β it comes
back as a rejection that says why. It is the classical first-order
calculus with equality (β§ β¨ β Β¬ βοΈ β β =, classical RAA
/ excluded middle), and every certification is re-validated by a
second, independent checker written in Go whose theorem
value has unexported fields β so no Axioma literal can forge one.
import "proof"
atomA: kpred("A", [])
t: certify([ assume(atomA), impI(1, 1) ], kimp(atomA, atomA), [])
proved(t) # β true
showTheorem(t) # β "β’ (A β A)"
import "proof" resolves the bundled library from
anywhere β no AXIOMA_PATH, no lib/ checkout,
and identically in the browser playground (the library is compiled into
the binary). import "proof.Core" and
import "lib/proof/Core.ax" name the same module; in a
source checkout an on-disk lib/proof/Core.ax overrides the
bundled copy so development edits are live.
A proof has three parts:
- Formulas are data, built with
k-prefixed constructors βkand/kor/knot/kimp/kiff/kall/kex/kpred/kv/keq/kin, and the valuekFalse(β₯). The prefix is required:and/or/not/forall/in/eqare Axioma keywords, so the object-language connectives can't reuse them. - A proof is a list of steps, each citing earlier steps by their 1-based line number β a Fitch/Lemmon derivation, written as data.
certify(steps, goal, catalog)is the trust boundary. It runs the proof, checks the last line equalsgoal, collects undischarged assumptions as Ξ, and returnsΞ β’ goalsealed β or{tag: "rejected", reason}.
Walk a proof of (A β§ B) β A:
atomA: kpred("A", [])
atomB: kpred("B", [])
t: certify([
assume(kand(atomA, atomB)), # 1. A β§ B (open assumption)
andEL(1), # 2. A (β§-elim left, from line 1)
impI(1, 2) # 3. (A β§ B) β A (discharge line 1)
], kimp(kand(atomA, atomB), atomA), [])
proved(t) # β true
len(hyps(t)) # β 0 (CLOSED β the assumption was discharged)
showTheorem(t) # β "β’ ((A β§ B) β A)"
The step builders cover the whole calculus: assume /
byAxiom / refl; mp /
impI; andI / andEL /
andER; orIL / orIR /
orE; notE / notI /
raa / falseE; ui /
exI / allI (with the eigenvariable proviso);
and eqSubst / iffI / iffEL /
iffER. Inspect a result with proved,
conclusion, hyps, whyRejected,
and showTheorem.
A proof that leaves an assumption undischarged certifies as a
conditional theorem with that assumption in Ξ
(A β’ A, not a dishonest β’ A). A non-proof
returns a legible reason, never a crash:
bad: certify([ assume(keq(kv("a"), kv("b"))) ], keq(kv("a"), kv("c")), [])
proved(bad) # β false
whyRejected(bad) # β "concludes a = b, not the goal a = c"
And proved recognises a real theorem by its Go type, so
a hand-written look-alike hash is rejected β certify is the
only thing that mints one.
A companion module, lib/proof/lemmas.ax, proves eleven
canonical theorems once at import and exports them as ready-made sealed
values (identity, β§/β¨ commutativity, excluded middle, double-negation
elimination, K, hypothetical syllogism, contraposition, =
reflexivity/symmetry/transitivity) plus a provenLemmas
catalog you can cite as axioms (each is a proven closed theorem = a
derived rule):
import "proof"
import "proof.lemmas"
showTheorem(lemDne) # β "β’ (¬¬A β A)"
showTheorem(lemContra) # β "β’ ((A β B) β (Β¬B β Β¬A))"
Scope. The kernel proves concrete instances
over named atoms/terms (not schemata), is classical, and its trusted
base is the whole Go interpreter β ideal for pedagogy, exploration, and
integration with the grounding ladder, but not a high-assurance
substitute for Coq/Lean. The mitigation (export a proof and re-check it
with an independent verifier) is built. Full guide and reference:
lib/proof/README.md. Worked tests: tests/axioma/proof/test_lib_proof_v1.ax,
test_lib_proof_lemmas_v1.ax.
32. Executable
Logic Engines (the logic namespace)
Axioma's logic surface is a portfolio of
fragment-engines behind one namespace,
[logic/<mode> | β¦]. Each engine runs a different
fragment of logic, and β this is the part most tools skip β
every answer reports the decidability regime it came
from, so you always know whether a result is a genuine
decision, a bounded search, a sound-but-incomplete
heuristic, or a principled refusal. There is no single
"decide any logic" engine, because for full predicate logic there
provably cannot be one; the namespace owns the pieces and routes between
them.
The mode table below is the public decidability map: every run
reports its regime in the returned SolveResult.
32.1 Held value vs. run
[logic | Ο] # the held-formula VALUE (parse-only, like hold(Ο)); @[logic | Ο] β "AST"
[logic/<mode> | Ο] # RUN Ο through an engine β a SolveResult
A run returns a SolveResult read with the possessive
accessors:
res: [logic/smt | x + 0 == x]
res's result # the verdict (a Boolean, a Set of answers, β¦)
res's regime # the decidability regime this answer came from
res's grounding # axiom / theorem / conjecture / datum
res's justification # a human-readable explanation
Landmine: a single-letter result variable named
rorbcollides with ther"β¦"/b"β¦"raw-string/bytes lexer prefixes when followed by's. Use a multi-letter name (res,ans, β¦).
32.2 The engines
| Mode | Fragment it runs | Regime |
|---|---|---|
[logic/sat | C] |
ALC description-logic satisfiability (β β Β¬ β β) | decidable |
[logic/asp | P] |
answer-set programming (disjunction, NAF, choice, aggregates) | decidable (grounded) |
[logic/sld | Ο] |
relational / Datalog / Prolog-style resolution | decidable (Datalog) / semidecidable (general SLD) |
[logic/holds | Ο] |
does a closed formula hold in the fact store? | decidable |
[logic/smt | Ο] |
quantifier-free equality + linear arithmetic + arrays + bitvectors + multi-sort (z3) | decidable |
[logic/valid | Ο] |
QF-SMT validity (Ο valid iff
Β¬Ο unsat) |
decidable |
[logic/prove | Ο] |
quantified SMT via z3 E-matching (sound-but-incomplete) | heuristic |
[logic/chase | P] |
the chase / DatalogΒ± β existential rules, materializes the universal model | decidable (weakly acyclic) / bounded(N) |
[logic/answer | P ? q] |
certain answers to a conjunctive query over the (possibly infinite) chase | decidable / bounded(N) / refused |
[logic/model | Ξ] |
positive finite-model finder; decides the EPR β*β* class | decidable (EPR) / bounded(N) |
[logic/auto | Ο] |
classify the goal and route it to the fitting engine | inherited |
32.3 SMT β =
and arithmetic finally run
[logic/smt | x + 0 == x] # β true (the algebra headline)
[logic/smt | x == y and x != y] # β false (unsatisfiable)
[logic/valid | (a == b) implies (f(a) == f(b))] # β true (EUF congruence is valid)
[logic/smt | x / 2 == 3] # β true (lifts to the reals, x = 6.0)
[logic/smt | (x band y) == x and (x bor y) == y] # β true (bitvectors, QF_BV)
[logic/valid | (bv(x, 8) + 1) > bv(x, 8)] # β false (8-bit overflow wraps)
[logic/smt | favorite(sort(a,"Person")) == sort(red,"Color")] # multi-sort EUF + NelsonβOppen
The fragment is chosen automatically and enforced β
a nonlinear x*y, a quantifier, or a cross-sort
== is rejected with a clean error, never a wrong verdict.
Quantifiers belong to [logic/prove] (below).
32.4 The chase and certain-answer querying
[logic/chase] runs existential rules (a head variable
not in the body invents a fresh labeled null) and returns the
universal model; [logic/answer] answers a query over it β
including the decided NO over an infinite chase that
materialization can never give:
ans: [logic/answer |
person(socrates).
parent(X, Y) :- person(X). # every person has a parent (existential Y)
person(Y) :- parent(X, Y). # β¦who is a person β an INFINITE chase
?
goal :- person(P), parent(P, Q) # "is there a person who has a parent?"
]
ans's result # β true
ans's grounding # β "theorem" (complete β decided by query rewriting, no model built)
A rule set that is neither weakly acyclic, guarded, nor sticky has
undecidable query entailment, so
[logic/answer] refuses with the theorem
(BeeriβVardi) and points you at the bounded [logic/chase]
materialization.
32.5 The quantified prover β honest about its limits
Quantified first-order logic with uninterpreted functions is
undecidable, so [logic/prove] is sound but
incomplete β its regime is always heuristic:
[logic/prove | (forall x, y. f(x) == f(y) implies x == y)
implies (f(a) == f(b) implies a == b)] # β true (z3 proves it)
[logic/prove | (forall x. f(x) == f(x)) implies (forall y, z. f(y) == f(z))] # β false
The crucial honesty rule: when z3 cannot decide a goal it returns
om (undetermined), never
false β absence of a proof is not a refutation. A
decided verdict is grounded conjecture, not
theorem; for a checked, theorem-grade certificate use the
Proof Assistant
(lib/proof).
32.6 Regimes β what an answer is worth
| Regime | Meaning |
|---|---|
decidable |
a true decision procedure ran to completion β the answer is final |
semidecidable |
a sound search β a positive answer is final; "not found" is not "false" |
bounded(N) |
a finite search up to size/depth N β "none up to N", not "none" |
heuristic |
sound but incomplete β a positive answer is trustworthy, absence is inconclusive |
refused(β¦) |
the request is provably impossible β the engine names the theorem and offers the legitimate adjacent move |
This is the operational form of Axioma is aware of its own limits: it never silently attempts the impossible (deciding arbitrary FOL validity, finding a model of any formula, deciding finite validity) β it names the wall and offers the move that is actually computable.
33. Algebraic Data Types
ML/Haskell-style sum types β a closed set of tagged
alternatives, each carrying its own positional fields. This is a second,
complementary route to variant data alongside the Concept system's
extends + partition; see the trade-offs at the
end of this section.
Declaring a sum type β
data
data Shape = Circle(Float) | Rect(Float, Float) | Dot
data Name = Ctor1(field, ...) | Ctor2(...) | Nullary
declares Name as a Concept whose instances are one of the
listed constructors. An n-ary constructor is a callable
that builds a value; a nullary constructor (Dot) is a bare
singleton β no call needed:
c: Circle(2.0) # calls the constructor
d: Dot # bare singleton, no parens
type(c) # β "Shape"
@c # β "Shape" (same as type())
c is Shape # β true
tag(c) # β "Circle" (the constructor name)
Circle(2.0) == Circle(2.0) # β true (structural equality β same tag + fields)
Circle(2.0) == Dot # β false
Ordering is structural (Haskell
deriving Ord): by constructor declaration
order first β for Shape that is
Circle < Rect < Dot β then
by fields left to right. The comparison operators and sort
/ min_by / max_by / sort_by all
agree:
Circle(1.0) < Circle(2.0) # β true (same tag, compare the field)
Circle(9.0) < Dot # β true (Circle declared before Dot)
Rect(1.0, 2.0) < Rect(1.0, 3.0) # β true (first field ties, second decides)
sort([Dot, Rect(1.0, 1.0), Circle(9.0)]) # β [Circle(9), Rect(1, 1), Dot]
None < Some(5) # β true (None is declared first)
Circle(1.0) < Some(2) # β Error (different data types don't order)
Field type annotations (Float) are
advisory in v1 β checked only under
--typecheck against literal arguments, not enforced at
construction time. An unannotated field
(data Option = None | Some(value)) is also legal.
v1 limitations. Monomorphic constructors only β no
type parameters (data Option[T] doesn't parse). A
constructor may not share its type's name
(data Foo = Foo(...) errors with a rename hint β the shared
environment namespace would shadow the type Concept itself); pick a
distinct constructor name (data Foo = MkFoo(...)).
Constructor
patterns in match and clausal func
area: func(s) [
match s with
| Circle(r) => 3.14159 * r * r
| Rect(w, h) => w * h
| Dot => 0
]
area(Circle(2.0)) # β 12.566...
# Nested patterns, wildcard fields, literal fields, `when` guards:
match Jus(Circle(9.0)) with
| Jus(Circle(r)) when r > 5.0 => "big"
| Jus(Circle(_)) => "small"
| Jus(_) => "other"
| None => "empty"
# Multi-clause func β same pattern language, first-match dispatch:
func area(Circle(r)) [3.14159 * r * r]
func area(Rect(w, h)) [w * h]
func area(Dot) [0]
An applied constructor pattern (Circle(r)) matches by
tag AND arity, and binds each field to the corresponding pattern; a
nullary tag (Dot) matches only that exact singleton.
match is total β a value that hits no arm
and no _ catch-all falls through to om, not a
crash (match Dot with | Circle(r) => r β
om), so a partial match is always safe to write.
One arm, several shapes β
| alternatives
Separate alternatives with | when one body serves
several patterns. They are tried left to right:
Day enumerates Mon, Tue, Wed, Thu, Fri, Sat, Sun
match d with
| Sat | Sun => "weekend"
| _ => "weekday"
match n with | 1 | 2 | 3 => "small" | _ => "other" # literals too
func weekend?(Sat | Sun) [true] # and in a func clause
func weekend?(d) [false]
Every alternative must bind the same variables β otherwise the body could name something that exists down one branch and not the other:
match s with | Circle(x) | Rect(x, _) => x | Dot => 0.0 # fine: both bind x
match s with | Circle(x) | Rect(w, h) => x | Dot => 0.0
# β or-pattern alternatives must bind the same variables:
# 'Circle(x)' binds x but 'Rect(w, h)' binds h, w
Names are compared as a set, so
[a, b] | [b, a] agrees; use _ for a component
one alternative has and the other doesn't. Inside a bracket the
| is already the cons pattern ([h | t]), so
parenthesise to nest an alternative group: (Sat | Sun).
Naming the whole β
pattern as name
as binds the whole matched value while the pattern keeps
destructuring the parts. It works anywhere a pattern does, nested fields
included:
match s with
| Circle(r) as c => str(c) + " has radius " + str(r) # both c and r bind
| _ as other => "not a circle: " + str(other)
match d with | (Sat | Sun) as day => "weekend: " + str(day) | _ => "weekday"
func label(Circle(r) as c) [str(c)]
Exhaustiveness +
redundancy under --typecheck
Static-only diagnostics β normal execution is completely unaffected,
whether or not --typecheck runs:
data Shape = Circle(Float) | Rect(Float, Float) | Dot
# missing Dot arm, no catch-all:
r: match Circle(1.0) with
| Circle(r) => r
| Rect(w, h) => w * h
# --typecheck β "non-exhaustive match on Shape: missing constructor(s) Dot
# (unmatched values fall through to `om`)"
# a `_` or bare-variable catch-all always suppresses the warning:
match Circle(1.0) with | Circle(r) => r | _ => 0.0 # clean
# a later arm whose tag an EARLIER unguarded arm already covers β unreachable
# (separate from exhaustiveness β this example also covers every constructor):
match Circle(1.0) with
| Circle(r) => r
| Circle(_) => 0.0
| Rect(w, h) => w * h
| Dot => 0
# --typecheck β "unreachable match arm: constructor 'Circle' already matched
# by an earlier arm"
# an unknown/typo'd constructor tag for the inferred type:
match Circle(1.0) with | Circl(r) => r | _ => 0.0
# --typecheck β "unknown constructor 'Circl' for type Shape
# (declared: Circle, Rect, Dot)"
A guarded arm's tag never counts as coverage (the
guard might fail at runtime, so exhaustiveness can't assume it fires) β
a match whose only Circle arm carries a when
still needs an unguarded fallback or _.
Annotate the parameter to check exhaustiveness inside a
function. --typecheck resolves the scrutinee's
type from a static shadow model, not full type inference β a direct
constructor call or bare nullary constructor resolves automatically, but
an unannotated function parameter does not, so a
match inside func(s) [ match s with ... ] is
silently skipped (no exhaustiveness check at all, not even a false
pass):
# NOT checked β `s` has no declared type, so the scrutinee's type is unknown:
area: func(s) [ match s with | Circle(r) => r | Rect(w, h) => w * h ]
# checked β the `:: Shape` annotation gives inferExprType something to read:
area: func(s :: Shape) [ match s with | Circle(r) => r | Rect(w, h) => w * h ]
# --typecheck β "non-exhaustive match on Shape: missing constructor(s) Dot ..."
Result /
Option β seeded ADT preludes
Result = Ok(value) | Err(error) and
Option = None | Some(value) are seeded globally β usable
with no declaration in user code, identically to any
data-declared type:
r: Ok(5)
type(r) # β "Result"
o: Some(5)
o is Option # β true
match r with | Ok(v) => v | Err(e) => 0 # β 5
match o with | Some(v) => v | None => -1 # β 5
If a script also writes
data Result = Ok(value) | Err(error) or
data Option = None | Some(value) itself (the exact same
signature the seed already produced), that's a safe no-op β not an error
β via the same idempotent re-declaration check that applies to any
data type.
|?> β
error-propagating railway composition
The pipe-try operator x |?> f recognizes the
Option/Result convention by tag name, not by a
registered type β so it works over the seeded
Result/Option AND over any user's own
data-declared type using the same
None/Some/Ok/Err
tags:
double: func(n) [n * 2]
Some(5) |?> double # β 10 (unwraps the field, calls double(5))
Ok(5) |?> double # β 10 (tag-generic β not Result-specific)
None |?> double # β None (short-circuits; double is NEVER called)
Err("x") |?> double # β Err("x") (short-circuits, value unchanged)
half: func(n) [Some(n / 2.0)]
Some(10) |?> half |?> double # β 10.0 (chains β half stays "in the railway"
# by returning Some(...) itself)
Unwrap fires only for a Some/Ok-tagged
value with exactly 1 field β any other shape (a
different tag, a 2-arity Some, or an unrelated constructor
like Circle(5.0)) passes through to the RHS
unchanged, matching |?>'s pre-existing
non-ADT behavior over
Error/none/om.
pipe_try never re-wraps the RHS's return value; staying "in
the railway" past a stage is the RHS's own job.
Comprehension constructor-pattern destructure
A constructor pattern works as a first-generator
target, alongside the existing single-var / tuple
((a, b) <- xs) / hash ({a, b} <- xs)
forms, across all four comprehension flavors and both surface forms:
maybes: [Some(1), None, Some(3), None, Some(5)]
[v | Some(v) <- maybes] # β [1, 3, 5] (None rows silently SKIPPED)
[v for Some(v) in maybes] # β [1, 3, 5] (Python pipe-less form)
{v | Some(v) <- maybes} # β {1, 3, 5} (set comp)
force((v | Some(v) <- maybes)) # β [1, 3, 5] (lazy comp)
[v | Some(v) <- maybes, v > 2] # β [3, 5] (combines with a filter)
A source element whose tag or arity doesn't match is silently dropped
β filter+extract in one step, the same convention hash-destructure uses
for a missing key. Scope: first-generator position
only, and only the applied form (Tag(field) <-)
β a bare nullary pattern with no parens (None <- maybes)
is not recognized here (the parser gate requires capitalized-IDENT
immediately followed by (, so it never reinterprets the
pre-existing bare-uppercase-generator-variable idiom, e.g.
{Y | Y <- parent("John", Y)}).
Concepts vs. sum types β which to reach for
Concept + partition |
data sum type |
|
|---|---|---|
| Variant set | Open β a new subtype can be added later | Closed β fixed at declaration |
| Structure | Inherited fields via has up the extends
chain |
Positional fields per constructor |
| Dispatch | if x is A then ... else if x is B then ... |
match x with | A(...) => ... | B => ... |
| Exhaustiveness | Not statically checked (open by design) | --typecheck catches a missing/redundant arm |
| Runtime extensibility | Yes β concepts can be declared dynamically | No β the constructor set is fixed at the data line |
Reach for data when the variant set is genuinely closed
and you want the type-checker to catch a missed case (parsers, small
calculators, protocol messages). Reach for Concepts +
partition for open-world domains that may grow new subtypes
over time (see Β§13 and the
Concepts vs. Sum Types chapter of the HtDP-in-Axioma textbook
for the full comparison).
VM notes
data declarations, constructor patterns, and
comprehension constructor-destructure are all
evaluator-only β --vm doesn't compile
*ast.DataStatement (the whole family inherits this
pre-existing boundary, same as relation/rule declarations).
|?>'s ADT-awareness is mirrored in the VM's
pipe_try case for forward compatibility, but since no
ConstructorValue can reach the VM today, that mirror is
presently unreachable β the VM's non-ADT |?> behavior
(over Error/none/om) keeps its
full, verified parity, unaffected.
The --typecheck notes above are the public contract for
ADT static diagnostics; VM support for the whole ADT family is a later
implementation phase.
34. Reference
Keyword index
| Keyword | Group |
|---|---|
: (bind), lambda, func,
fn, define |
Bindings & functions (bind a value with :) |
if, then, else |
Control flow |
true, false, none,
null, om, Ξ© |
Literals |
and, or, not,
implies, iff |
Logic |
forall, exists, in |
Quantifiers |
union, intersect, difference,
subset |
Set ops |
concept, has, extends,
delete, is, is |
Concept system (creation uses concept only;
exists is reserved for the existential quantifier) |
data, match ... with, tag,
|?> |
Algebraic data types (Β§33) β data X = A(v) | B,
constructor patterns, railway composition |
axiom, postulate |
Knowledge tiers |
insert, forget, retract,
cancel, uncancel |
Mutation (relation named by string) |
transaction_begin/commit/rollback |
Transactions |
head whenever body /
typically head whenever body (primary);
head if body, rule~ head if body,
<=/<== (:-),
<~~, ==>, ~~> |
Rules (strict / defeasible Γ backward / forward) |
grounding, truth_kind, why,
proof, rules_of, challenge,
challenged, canceled |
Provenance & introspection |
try, attempt, otherwise,
or else, error, raise,
error? |
Errors as values (Β§29) |
understand, examine, abduce,
[ model | β¦ ] |
Cognitive kernel (Β§30) |
kleene, belnap, lukasiewicz,
intuit3 |
Multi-valued-logic value constructors |
necessarily, possibly |
Modal |
always, eventually, next,
until |
Temporal |
knows, believes,
common_knowledge |
Epistemic |
obligatory, permitted,
forbidden |
Deontic |
dup, swap, rot,
over, drop, nip,
tuck, stacklength, erase |
Stack |
is/same, is/identical,
is/property |
Russell's copula |
venn, fullform, treeform,
tableform, graphform |
Visualization & the *form family |
Runtime reflection β the discovery vocabulary
The self-describing surface (Β§25 has the full story with examples). One row per question the language answers about itself:
| Question | Ask | Notes |
|---|---|---|
| What is this word/type? | doc word / doc(word) |
type cards for the 12 core types; keywords via
doc("if") |
| What is this value? | describe(x) |
per-type fact card; points onward at doc and
functions |
| What exists at all? | builtins(), concepts(),
bindings(), keywords() |
catalogs; 1-arg forms are membership checks |
| Where is anything about β¦? | apropos("term") |
searches names AND documentation text |
| What can I do with a type? | functions(Integer) β‘ methods(Integer) |
curated per-type builtin catalog (12 card types) |
| How do I call this? | signature(f), arity(f),
parameters(f) |
spec-backed for builtins; user functions too |
| What is this function's definition? | source(f) |
reconstructed source String β eval(source(f))
round-trips |
| What does this source parse to? | ast("src"), parse(code, [mode]) |
parse without running; '(β¦) quote is the lexical
form |
| Does this compile to the VM? | compile(src) |
callable on success; catchable Error names the VM boundary |
| Which limits does a type have? | Float.max, Byte.max,
Integer.bounded |
live members; Integer.max is a teaching error |
Type catalog β every
name type() / @ can return
The canonical TitleCase names, grouped. Each is backed by a
same-named primitive-type Concept where one exists, so
type(x), @x, and x is Name agree
(typeNameForObject, evaluator/builtins.go, is the
single source).
| Group | Names |
|---|---|
| Numbers | Integer, Float, Rational,
Complex, Infinity β Β§4 |
| Text & binary | String, Byte, Bytes β Β§4 |
| Booleans & bottoms | Boolean, Null
(none/null), Om
(om/Ξ©) β Β§4 |
| Multi-valued truth | Kleene, Lukasiewicz, Belnap,
Intuit3 β Β§9 |
| Collections | Array, Tuple, Set,
InfiniteSet, Bag, Dictionary,
Stack, Generator (lazy) β Β§5, Β§7 |
| Math & data containers | Matrix, Tensor, DataFrame β
Β§5 |
| Lexer scalars | URL, Email, File,
Date, Time, Money,
Pair, Issue, Percent,
Word, GetWord, QuoteWord β Β§4 (Tag exists but is shadowed by
the <β¦> natural-language literal) |
| Time values | DateTime, Duration β the
builtin:datetime package, Β§4 |
| Callables & code | Function, Builtin, Reference
(Β§21), AST (Β§19), Symbol,
Expr, SymExpr (the CAS layer, Β§28) |
| Knowledge & logic | Concept, ConcreteEntity (Β§13), Relation,
RuleClause (Β§14, Β§16),
Grounding, Kind (Β§16),
Proposition, Formula (Β§8), ConceptExpr (DL, Β§13), Epistem (Β§15),
CheckedTheorem (Β§31),
SolveResult (the PΓ³lya solver), Glyph (Β§3), ConceptualGraph,
Error (Β§29) |
| Natural language | NaturalLanguage (<β¦>),
ConstrainedLanguage, Dialect,
NSMExplication, CognitiveWord |
| ADT values | report their data-type name
(Circle(3.0) β "Shape") β the constructor tag
is the separate tag() axis, Β§33 |
| Everything else | internal subsystem objects (Kripke/epistemic models, games, fuzzy
sets, proof objects, β¦) currently report "Unknown" |
Refinement table
| Form | Effect |
|---|---|
declare/persist x = v |
Save to .axioma_session.bin (use =, not
:) |
declare/transient x = v |
Discard at session end |
axiom/persist |
Save to cascade.db |
axiom/transient |
Session-only axiom |
postulate/persist |
Save to cascade.db |
postulate/transient |
Session-only postulate |
Tag-filter values for comprehensions
@axiom, @postulate, @theorem,
@conjecture, @hypothesis, @datum,
@canceled, @all, @*
File locations
- Knowledge base β
cascade.db(project) or~/.axioma/axioma.kb(MCP server) - Session state β
.axioma_session.json - Diagrams β
diagrams/venn_diagram_TIMESTAMP.png - Logs β
~/.axioma/mcp.log - PID β
~/.axioma/mcp.pid - Examples β
tests/axioma/**/*.ax,tests/axioma/showcase/**/*.ax,lib/**/*.ax
Error messages
| Error | Cause | Fix |
|---|---|---|
identifier not found: X |
Undefined variable | Define or check spelling |
Parser errors: ... |
Syntax error | Check parentheses, brackets, operators |
wrong number of arguments |
Arity mismatch | Check function signature |
type mismatch |
Incompatible types | Convert or check operand types |
division by zero |
n / 0 |
Check denominator |
cardinality violation: <C>.<p> |
Slot over-assignment | Last-write-wins; check _cardviolation_* |
See also
Axioma.mdβ feature index and quick referenceAxioma Elements.mdβ high-level element-group mapdocs/f-logic-unification.mdβ unified frame-logic / bilattice / G3 design docdocs/core-language.mdβ compact core-language referencedocs/computational-core.mdβ verified computational subsetdocs/feature-maturity.mdβ maturity tiers and release-facing caveats
Axioma Programming Language v0.9 Β Β· Β Calculemus! β Let us calculate. (Leibniz) Β© 2024β2026 β Mathematical Computing, Logic, and Knowledge