Reference

The Axioma Manual

The complete language reference — from binding a name with a colon to the bilattice truth values, the concept system, epistemic grounding, and the full CLI. One canonical source, rendered here and as a PDF.

πŸ“– 34 sections Β· 160 pages Β· generated from the canonical source. Download PDF →

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 Stack type 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 'w word literal), homoiconicity (code as data), and the S-expression view of the AST; Mathematica β€” fullform for 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} and B = {2, 4, 6} are set variables. The right-hand side decides concept-vs-value: a concept { ... } RHS names a Concept (uppercase required β€” Stock: concept { ... }; a lowercase LHS with a concept RHS 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.

A and E are ordinary identifiers, not quantifier shorthands. Quantify with forall / exists, the glyphs βˆ€ / βˆƒ, the backtick digraphs `forall / `exists, or βˆƒ! (unique existential). E! (the free-logic existence predicate) is a separate token.

There is no let or :=. Bind with : (canonical) or =. Writing let x = value or x := value is a syntax error with an inline hint pointing at x: 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 (and declare/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/mininteger constants β€” a largest integer does not exist. (Python 3 removed sys.maxint for the same reason; Julia has no typemax(BigInt); Haskell's Integer has no Bounded instance.)
  • Hex/binary/octal literals are magnitudes, not bit patterns: 0xFFFFFFFFFFFFFFFF is 2βΆβ΄βˆ’1 (a positive number), not Lua's -1.
  • Bit operators (band/bor/bxor/bshl/bshr) use Python's infinite-two's-complement reading: bshl is exact at any count (1 bshl 63 = 2⁢³, 1 bshl 100 = 2¹⁰⁰), and bshr is an arithmetic shift that drains to 0 (or -1 for 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 an Error value, not a crash.
  • The representation is invisible: type(2^100) is "Integer" like any other integer. Mixing with a Float converts 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 (fi)
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

  • Byte displays as decimal. byte(0xFF) prints as 255 β€” the Python / Go / Rust convention for byte values. For a hex rendering use bytes_to_hex(bytes(b)). (Bytes keeps the b"..." literal form with \xff escapes.)
  • Encoding-aware separation. Bytes doesn't carry encoding metadata; conversion to String is explicit and fails on invalid input.
  • Immutable. Operations return new Bytes rather 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 String indexes by CHARACTER (rune), a Bytes by byte. "ÀÀb"[2] β†’ "Γ€" (the 2nd character; negative indices count characters from the end, "rΓ©sumΓ©"[-1] β†’ "Γ©"), consistent with nth / substring / the slice forms / the ordinal accessors β€” so s[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 across Bytes / Array / String / Tuple β€” identical to x[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 idiom arr[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-less xs[2..] slices to the end (≑ xs[2:] β€” arrays, strings, tuples). Descending selection spells its step explicitly β€” xs[5..1..-1] β†’ reversed (the stepped x[lo..hi..step] form keeps the directional range path, evaluator-only); a step-less x[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 as pack / unpack builtins using Python's struct-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 / B 1 Integer / Byte Integer / Byte signed / unsigned 8-bit
    h / H 2 Integer Integer signed / unsigned 16-bit
    i / I 4 Integer Integer signed / unsigned 32-bit
    q / Q 8 Integer Integer signed / unsigned 64-bit
    f 4 Float Float IEEE 754 single
    d 8 Float Float IEEE 754 double
    s N String / Bytes Bytes counted byte field, pads/truncates
    c 1 Bytes(1) Bytes(1) single-byte field
    x 1 (none) (none) pad byte β€” consumes 0 values
  • Count prefix: 4B = four unsigned bytes (consumes 4 args). For s, the count is the byte-length of the field: 4s packs/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)

:x is the get-word; @x is not. @ is the type-of sigil, so @x returns "Integer" (the type of x), while :x returns 42 (the value). Use :x to 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), not exists(...) β€” the bare word exists lexes as the existential quantifier (βˆƒ), so exists("/p") is a parse error. file_exists matches the io package's IO.file_exists.
  • Deletion is remove(path), not delete(...) β€” delete is the reserved retract keyword.
  • Tag (<tag>) is shadowed by the natural-language <…> literal, so type(<html>) is "NaturalLanguage", not "Tag" β€” treat <tag> as unavailable from source.
  • Absolute %/abs/path does not lex (the character after % must be alphanumeric); use the String form read("/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, not head / tail. head and tail are 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, and rest does not deep-copy, so a recursive first/rest descent is linear.
  • In a match arm, [h] is a block, not a one-element array β€” it evaluates to h. 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 to Dictionary and fully retired the old name β€” ObjectMap is now an undefined word, not an alias). Dict is accepted as a short alias in type annotations (x :: Dict) and is checks, matching the dict() constructor, but type()/@ always report the canonical Dictionary.

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. identity is the identity function (identity(42) β†’ 42), not an identity-matrix constructor β€” write matrix([[1, 0], [0, 1]]). And there is no matrix-inverse builtin (inverse is the relation/set inverse) β€” for linear systems use solve(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)

  1. Function application: f(x)
  2. ^ (exponent), not, & (address), * (deref)
  3. unary - (negation)
  4. *, /, %, //, mod, modulo, div
  5. +, -
  6. Set: union, intersect, difference
  7. Comparison: <, >, <=, >=, ==, !=, subset, in, is
  8. and
  9. or, xor / ⊻, ⊼ (nand), ⊽ (nor)
  10. implies, iff
  11. |> / |?> (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 no y := expr or let y = expr form.

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.

Tests: tests/axioma/comprehensions/test_python_superset.ax

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

lazy is a soft keyword β€” it is still usable as an ordinary name (lazy: 5, f(lazy)). It binds at prefix precedence, so lazy 1 + 2 is (lazy 1) + 2; parenthesize to defer the whole expression. lazy and declare are 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.

Tests: tests/axioma/comprehensions/test_hash_destructure.ax

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_limit_offset.ax

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 0 and 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:

  1. Truthiness is designation. if/while on 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}. So if βŠ₯ᡇ … and if ?ᡏ … take the else-branch, and if x always agrees with designated(x). (Untyped om keeps its SETL truthiness β€” see the dual-null note in Β§4.)
  2. == / != 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 is iff (?ⁱ iff ?ⁱ β†’ ⊀ⁱ). Membership composes too: ⊀βŠ₯ᡇ in Belnap.values β†’ true.
  3. 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 unknown is still represented by om (Ξ©) at the operator level β€” the existing om and true tables are unchanged. kleene(...) / ?ᡏ is the typed form: it normalizes to om for evaluation, runs the same K3 tables, then re-wraps logic results back into a Kleene so they stay introspectable (mirroring how belnap/lukasiewicz/intuit3 operators return their own type). No asymmetry any more: ?ᡏ == om is true, and both if ?ᡏ and if om are falsy (designation β€” om folds 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

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 the args:, result:, and how many inputs preceded it;
  • ?ᡇ β€” nothing checkable: no contract, no input source, or every input was skipped by requires: (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, so 3 |> f |> g is a one-shot computation with a 3 baked into it. f |> g computes g(f) β€” a type error, not a pipeline. Use compose(f, g) or f >> g when 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, so 5 is Integer and x :: Day run 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 is copula tests Russell's predication (∈, rex is Dog) and class inclusion (βŠ†, Dog is Animal); a defines predicate or a boundary turns 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 and check that it does its job β€” and an invariant: 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 isValidConceptName check every other concept-creation surface uses. tc: concept { ... } errors with concept name 'tc' should start with an uppercase letter. Use TC: concept { ... }. The bare-RHS form is also the only form that infers a name; anonymous concept { ... } 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 / is slot inline. Follow up with Stock is Asset on the next line, or use concept 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 canonical concept Sage { boundary~: ... } block form, or follow up with the statement-level Sage defines~ { ... } rule.
  • Namespaced names: define concept FinancialKB.Stock = ... is not supported (the define parser produces a flat []*Identifier symbol list). Use the prefix concept 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 Concept at statement level (TitleCase LHS) β†’ parser error (use concept 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 (including X is Concept as 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 Concept verifies 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 / entity forms (a Stock {}, an Item {}, entity Gadget {}) β€” all three produce the same AST and runtime value. There is no object keyword.

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 to it.<slot>.
  • is Concept in prefix position desugars to it is Concept.
  • and / or / not and 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 Both results 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 as defines, 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 English
  • unify + paraphrase β†’ the canonical entity's slot values are resolved before placeholder substitution
  • Subclass + paraphrase on 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/identity slot 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 unify machinery β€” 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 is must precede the cumulative has (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 cumulative keep 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 with Undefined word: parent. Declare the relation first (after which a bare parent(...) call adds a fact), or prefix with assert. 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-relation form). A bound TitleCase argument is a real thing (a type Concept, a declared relation, Thing/Nothing), so calls like doc(Integer), tableform(Edge), or satisfiable(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"), not grounding(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 runtime insert(...) 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 is datum.

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: delete and retract are reserved keywords. Use forget for the function-call form. The statement form retract [...] 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 Stack value β€” construct it with s: a Stack or with stack(); both build a real Stack (@s β†’ "Stack"). stack(c) additionally converts an array, set, tuple or finite range, with position 1 as the top: stack([1, 2, 3]) is Stack[1 | 2 | 3], and array(s) / set(s) / tuple(s) convert back. (stack_new() was the older nullary-only spelling; retired July 2026 β€” it now errors with a pointer at stack().)
  • 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 active trace binding shows both, and leaves binding running afterwards β€” restore puts 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 with clear_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:

  1. alias makes the grammar adaptive (the decisive one). The parser's parseAliasStatement installs the mapping into the live lexer (Lexer.AddAlias), and identifier lexing consults that table on every subsequent token β€” so after alias myif = if, the word myif tokenizes as the if keyword for the rest of the same source stream (verified: alias myif = if + alias mythen = then + alias myelse = else makes myif 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: alias re-lexes identifiers onto existing tokens; it cannot introduce new syntactic forms (see the limitations table above).

  2. Recognition is PEG-style, not CFG-style. The hand-written recursive descent + Pratt parser carries a dozen-plus SaveState/RestoreState speculative-parse sites β€” colonHeadsSetComprehension trial-parses to split the British set comprehension {x : x <- s} from a hash literal {k: v}; pipeStartsConsTail splits the cons pattern [H | T] from a comprehension [h | h <- xs]; dotBodyIsConcept splits the DL restriction βˆƒhasChild.Doctor from 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.

  3. 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 ($5 Money vs $name guard, %data File vs 7 % 2 modulo, #123 Issue vs #hello QuoteWord). 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 5 is 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 planets
  • Indiscernibility of Identicals β€” identicals agree on every predicate β€” runs the other way and is what keeps is/same finer than ==. 1 is/same 1.0 is false because exact? and type are 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. @x returns the type of x (@42 β†’ "Integer"), identical to type(x). Plain x already 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 [python is a registered dialect tag. Recognized tags: python (alias py); the secondary subprocess runners julia, rlang (R), js, lua, lisp (Common Lisp via sbcl; aliases cl / commonlisp), the compiled runners pascal (fpc; alias pas) and clang (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, model are 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 from r, July 2026) and C by clang, never c: while r was 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-line defs, 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 --vm and 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 Axioma Error). Print with format / princ / print; a bare (+ 1 2) returns "" because --eval does not echo a form's value β€” for a typed value back, use /eval mode (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 to Integer, 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_LISP overrides 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 β†’ sbcl on PATH.
  • 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 #\x character 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 to OpLangBlock and 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 program compiles verbatim. A headerless fragment ending in end. β€” declarations plus a main block, the shape book chapters print β€” gets a program header prepended. Bare statements (writeln(2 + 2)) get a full program/begin/end. skeleton. A unit body is rejected with guidance (units compile to libraries, not runnables). Compile errors return as catchable Axioma Errors 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/readln book programs need their inputs replaced with constants), bounded by AXIOMA_PASCAL_TIMEOUT seconds (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 catchable Error carrying 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') β€” so writeln('C:\TP\BIN') reads correctly; " is not a string delimiter; and { … } / (* … *) / // … comments may contain unpaired apostrophes ({ don't panic }). array[1..10] bounds and a[i] indexing balance the block's bracket depth naturally. Residual limit: a bare unbalanced ] outside any string/comment closes the block early.
  • AXIOMA_PASCAL overrides the compiler binary (unset β†’ fpc on PATH; 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 /mode form compile to OpLangBlock and run identically under --vm.

Lua specifics.

  • The body runs via the system interpreter β€” lua on PATH (macOS: brew install lua), overridable with AXIOMA_LUA (e.g. luajit or a pinned lua5.4). Runs are bounded by AXIOMA_LUA_TIMEOUT seconds (default 30 β€” while true do end cannot hang the interpreter), and stdin is at EOF, so io.read() returns nil instead 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) and a[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 to OpLangBlock and run identically under --vm.

C specifics.

  • [clang | … ] compiles the body with the system C compiler β€” cc on PATH (macOS: xcode-select --install), overridable with AXIOMA_CC (e.g. gcc-14) β€” and runs the produced binary. The tag is clang, never c (the one-letter ban; it reads "C language" the way golang reads "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; also c89/c90/c11/c17/ c23 and the gnu89/gnu99/gnu11/gnu17/gnu23 variants; 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) plus int main(void) { … return 0; } β€” so [clang | printf("%d\n", 2 + 2); ] just works, and <math.h> functions link (-lm is passed where needed). A fragment that opens with preprocessor directives but defines no main is rejected with guidance (wrapping #include inside main would be invalid C).
  • Compile cache: a successful compile is cached keyed on (source, mode, compiler identity) β€” identical blocks skip cc and only re-run (~2 s cold β†’ ~15 ms warm for a five-block script). Failures are never cached. Disable with AXIOMA_CC_NOCACHE=1; clear by deleting ~/Library/Caches/axioma/clang. Runs are bounded by AXIOMA_CC_TIMEOUT seconds (default 30) with stdin at EOF, so scanf-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, and a[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. For B == "axioma" the translated source is parsed and Eval'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 by double(5) work). For foreign B, the translated source runs through the FFI in /eval mode 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-worker to disable. Each block then spawns its own fresh python3 -c process (~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:

  1. py.reset() β€” clears worker globals between blocks without leaving worker mode:
    r1: [python | x = "first" ]
    py.reset()
    r2: [python | print('x' in dir()) ]    # False
  2. Use a unique prefix for variables in self-contained snippets, or wrap the snippet in a function so its locals don't escape.
  3. --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.call to 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.sqrt is the Go implementation, never a foreign runtime;
  • a language-prefixed name is explicitly FFI β€” python.math.sqrt runs CPython's math.sqrt through 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

try binds 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. Thus try(risky()) is Error ≑ (try(risky())) is Error β†’ true, matching the bind-it-first idiom e: try(risky()); e is Error. To make a whole binary computation optional, parenthesize it: try (a / b). attempt and the otherwise operator 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 / attempt are evaluator-only β€” under --vm they report a clean compilation not implemented. The value-level builtins error() / 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) :- B makes H iterable (for y in H), queryable, and introspectable (@H == "Relation") with no separate relation H declaration β€” 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-accessible ObjectMap ({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 value kFalse (βŠ₯). The prefix is required: and / or / not / forall / in / eq are 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 equals goal, collects undischarged assumptions as Ξ“, and returns Ξ“ ⊒ goal sealed β€” 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 r or b collides with the r"…" / 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 Programming Language v0.9 Β  Β· Β  Calculemus! β€” Let us calculate. (Leibniz) Β© 2024–2026 β€” Mathematical Computing, Logic, and Knowledge