Argon
Argon is a knowledge-representation language and graph database. You declare an ontology — concepts, the relations between them, and the rules that derive new facts — and Argon compiles it to a content-addressed artifact you can query, reason over, mutate, and serve.
This book is the surface specification: the syntax and meaning of an Argon program — the precise reference, chapter by chapter, in the technical specification and its appendices.
For a guided, motivated tour of the whole language, read The Argon Book, the teaching narrative (spec/book/). To watch real packages compile and run, see Argon by Example (examples/).
Two companion sources of truth sit beside this book:
- The Lean 4 mechanization (
spec/lean/) is canonical for the substrate — the type system, reasoning semantics, and decidability results. Where the Lean and this book disagree on something the Lean covers, the Lean wins; the book is the bug. - RFDs (Part II) record design decisions and their rationale. They are history, not normative spec.
Each chapter leads with a grammar fragment and a worked example before the prose. Cross-references are named links; each names the chapter or section it points to.
Toolchain: installation, channels, and versioning
How Argon is installed, versioned, and switched. oxup is the toolchain
manager (rustup-style); ox/oxc/ox-lsp/oxfmt are the tools it dispatches.
Install
curl -fsSL https://argon.sharpe-dev.com/install.sh | sh
Installs oxup into ~/.argon/bin, links the tool shims, and installs the
stable toolchain. Ensure ~/.argon/bin is on PATH.
Platforms: macos-arm64, linux-x86_64, linux-aarch64.
Channels
| Channel | Built from | Cadence |
|---|---|---|
stable | a vX.Y.Z tag on main | per release |
nightly | main tip | daily, 04:00 UTC |
dev | main tip | every merge to main (coalesced); also on-demand per branch |
Version strings
X.Y.Z is the in-development line; dev/nightly are prereleases of it (they
sort ahead of the last stable). oxup and the toolchain share one version.
| Channel | Format | Example |
|---|---|---|
| stable | X.Y.Z | 0.2.2 |
| nightly | X.Y.Z-nightly.<UTC-date> | 0.2.2-nightly.2026-06-16 |
| dev (main) | X.Y.Z-dev.<UTC-timestamp> | 0.2.2-dev.20260616T034155Z |
| dev (side branch) | X.Y.Z-dev-<branch>.<short-sha> | 0.2.2-dev-feat-x.c6b2b3fa6 |
Distribution layout
Served from https://argon.sharpe-dev.com (S3 origin behind CloudFront):
/install.sh installer
/toolchains/<version>/<platform>/argon-<version>-<platform>.tar.gz(.sha256)
/dist/channel-<channel>.toml current version per channel
/dist/index-<channel>.json all published versions
/oxup/latest/<platform>/oxup(.sha256) oxup self-update (stable)
/oxup/<channel>/latest/<platform>/oxup(.sha256) oxup self-update <channel>
/editors/vscode/<version>/argon-<version>.vsix(.sha256) editor extension
/toolchains/<version>/is immutable. dev/nightly tarballs are pruned 30 days after publish; stable is kept indefinitely.index-<channel>.jsonis newest-first: an array of{ version, released, platforms[], sha256, available }. A pruned build stays listed with"available": false.
Toolchain spec
A spec identifies a toolchain to install/use/+<spec>:
| Spec | Resolves to |
|---|---|
stable / nightly / dev | the channel’s current version |
<channel>:latest | same, explicit |
<channel>:<date> | the channel build for that day (nightly:2026-06-16) |
<full-version> | exact (0.2.2-nightly.2026-06-16, 0.2.1) |
latest | alias for stable |
oxup commands
oxup use <spec> install if needed, then set as default
oxup default <spec> alias for `use`
oxup install <spec> install without changing the default
oxup uninstall <spec> remove an installed toolchain
oxup update [channel] re-fetch a channel's latest
oxup list installed toolchains (default marked)
oxup list --available [--channel C] [--limit N] published versions
oxup search <pattern> search published versions across channels
oxup show active toolchain, default, installed
oxup which <tool> [--toolchain <spec>] resolved binary path
oxup self-update [channel] update oxup itself (stable; or dev/nightly)
oxup extension install|uninstall|list editor extension (matches the active toolchain)
--json is available on list, search, and show (for tooling).
Selecting a toolchain (per invocation)
First match wins:
OXUP_TOOLCHAIN=<spec>+<spec>as the first argument — e.g.oxc +nightly buildox-toolchain.tomlin the working directory or a parent —[toolchain] channel = "<spec>"~/.argon/settings.toml—[default] channel- the built-in
stable
A missing toolchain is fetched automatically on first use.
~/.argon/
~/.argon/
bin/
oxup the manager
ox oxc ox-lsp oxfmt symlinks → oxup
toolchains/<spec>/
bin/ ox oxc ox-lsp oxfmt
share/std/ bundled stdlib packages
manifest.toml version, platform, components
provenance.toml source (cdn|local), origin, sha256, install time
settings.toml [default] channel, [extension] state
ox/oxc/ox-lsp/oxfmt on PATH are symlinks to oxup; oxup reads
argv[0], resolves the active toolchain, and execs
~/.argon/toolchains/<spec>/bin/<tool>.
Tools
| Binary | Role |
|---|---|
ox | CLI driver — build, query, serve |
oxc | compiler |
ox-lsp | language server |
oxfmt | formatter |
Formatter notation policy
Argon’s dual notation (Unicode notation) lets source mix Unicode glyphs (∀ ⊑ → ⊞) with their ASCII spellings (forall <: -> box_plus); both lex to the same token. The formatter can normalize a file to one form, governed by [fmt] notation in ox.toml:
[fmt]
notation = "preserve" # | "unicode" | "ascii"
preserve(the default) never rewrites between the two forms — a file is formatted as typed, so mixed notation is left untouched.unicoderewrites every ASCII spelling to its glyph (forall→∀);asciidoes the reverse. The rewrite is lossless and round-trips, because both forms produce the identical token.
The policy resolves through a nearest-wins cascade (rustfmt/prettier-style): a package’s own [fmt] overrides a [workspace.fmt] at the workspace root, which overrides the global user config. The notation table itself is the single [[notation]] source shared with the lexer aliases and editor input methods. Notation is policy (a per-project taste call); spacing and layout are the Argon convention and are not configurable.
Editor support
Everything an editor knows about an Argon program comes from one place: the language server, ox lsp. The reason is the meta-calculus. Argon’s surface vocabulary is user-extensible — a package declares its own metatypes, concepts, relations, and constructs, so the set of meaningful words in a file is not fixed by the language. A static grammar can color and brace-match the punctuation, but it cannot know that Obligation is a concept or that owes is a relation; only the resolver that elaborated the package knows that. So all the language-aware behavior — diagnostics, hover, jump-to-definition, the right token colors — lives in the server, and editors are thin clients of it. One server, written once, drives every editor; an editor plugin is just an LSP client plus a syntax floor.
What ox lsp provides
ox lsp speaks LSP over stdio and advertises:
| Capability | What it gives the editor |
|---|---|
| Diagnostics | The full ox check diagnostic set, live as you type (debounced) |
| Hover | The type / declaration of the symbol under the cursor |
| Go-to-definition | Jump to where a name is declared |
| Semantic tokens | Vocabulary-aware coloring the static grammar cannot produce |
| Completion | Member completion after ::, and identifiers |
| Document symbols | The file outline / breadcrumbs |
| References | Find-usages across the workspace |
Inlay hints and code lenses are not built yet. Because the diagnostics are the server’s, they always match the toolchain that produced them — there is no second, drifting implementation of the checks.
Installing it
oxup installs and version-locks the editor integration, so the editor support matches the active toolchain:
oxup extension install # auto-detect every installed VS Code-family editor
oxup extension install --editor neovim # or name one explicitly
oxup extension uninstall --editor emacs
oxup extension list # detected editors + the installed version
The two editor families install differently because they expose different contracts:
- VS Code family (VS Code, Cursor, VSCodium, VS Code Insiders, and the browser-hosted code-server) have an
--install-extensionCLI.oxupfetches the.vsixstamped with the active toolchain version from an immutable CDN path, sha256-verifies it (the same fail-closed discipline as the toolchain fetch), and installs it. A user onstable0.2.2 gets the 0.2.2 extension, not “latest”. - Neovim, Vim, and Emacs have no extension-CLI contract, so
oxupinstalls them by file placement: it writes the embedded plugin files into the editor’s package directory and adds an idempotent, oxup-managed block to the init file (between>>> argon (oxup-managed) >>>sentinels) that points the client at the active toolchain’sox lsp. Re-running updates the block in place and leaves everything outside the sentinels untouched;uninstallremoves both.
oxup init and oxup update auto-install and refresh the integration after the toolchain is placed (oxup init --no-extension opts out). A failed editor install is a soft, one-line warning — the toolchain is what matters — never a hard error.
The VS Code extension ships the Argon “A” glyph as the .ar file icon. The Neovim and Emacs plugins cannot reuse that SVG (their file explorers draw a Nerd Font glyph, not an image); their READMEs give a suggested glyph-and-color registration keyed on the .ar extension.
Environment
| Variable | Effect |
|---|---|
OXUP_TOOLCHAIN | override the active toolchain |
OXUP_DIST_URL | override the distribution base URL (mirror, offline, testing) |
ARGON_CONFIG | path to the global ox user config (formatter notation cascade) |
Scope
Argon is a programming language for declaring and reasoning over a richly-typed knowledge graph at the data-system layer. The canonical specification of the language core lives in Lean 4 under spec/lean/; this document specifies the surface in prose. Where they disagree, the Lean wins.
The language is built from five atoms — meta-calculus, constructs, rule, trait, macro — and nothing else. Every surface form lowers to one of them. Argon has no surface for IO, threads, network, or filesystem; those live in the host runtime. Tenancy and multi-tenant orchestration are application concerns, not language concerns.
Lexical structure
Source
UTF-8. Source files have extension .ar. Non-ASCII text is admitted in identifiers
(Identifiers, UAX #31), comments (Comments), and string/char
literals (Literals). Operators and keywords have a canonical ASCII spelling and an equivalent
Unicode form — Lean-style dual notation (Operators): ∀ is forall, ⊑ is <:, → is ->,
⊞ is box_plus. The two are interchangeable surface spellings of the same token, so a
program may be written in either (or a mix), and editors enter the Unicode forms through a
\abbrev input method (\forall → ∀). Delimiters and the remaining punctuation are ASCII.
Comments
//— line comment, not extracted byox doc.///— outer doc comment, attaches to the next item.//!— inner doc comment, attaches to the enclosing module./* … */— block comment, may be nested. Not extracted byox doc. Comments out arbitrary tokens including//-prefixed lines, useful when temporarily disabling sub-expressions.
Identifiers
Identifiers follow UAX #31: an identifier starts with a
XID_Start character or _, and continues with XID_Continue characters. The ASCII subset
[A-Za-z_][A-Za-z0-9_]* is the common case and the recommended style. An identifier’s text is its
exact source bytes. NFC canonical equivalence (so that a precomposed é U+00E9 and the
decomposed e + combining acute U+0065 U+0301 would denote the same name) and a mixed-script
confusable warning (UAX #39, Latin A U+0041 vs
Cyrillic А U+0410) are the RFD 0034 name-resolution design (Out of scope). Conventions: snake_case for value bindings, fields, and rules (fn, derive, query, mutate, check); CamelCase for types, metatype-introduced concepts, and metarel-introduced relations; lowercase (typically snake_case) for the metatype- and metarel-introducing keywords themselves (kind, role, rel, mediation, …); SCREAMING_SNAKE_CASE for stdlib constants. Conventions are not enforced.
Operators
| Token | Meaning |
|---|---|
: | typing / instantiation (x: T, Dog : AnimalSpecies); metatype-body axis binding (rigidity: rigid) |
<: | specialization, trait inheritance, trait bound (⊑ alias) |
:: | qualification — path or axis::value |
:- | Datalog rule-body separator |
=> | rule head, match arm, check-rule diagnostic emission |
= | value assignment, single-expression body, kind-level value |
? | optional sugar (T? ≡ Option<T>); error propagation on Result |
.., ..= | range, cardinality |
-> | rule return type |
+, * | arithmetic; transitive / reflexive-transitive closure on role paths |
-, / | arithmetic |
==, !=, <, <=, >, >= | comparison |
&&, ||, ! | boolean (expression context) |
, | conjunction in Datalog body; tuple/list element separator |
; | statement terminator |
| | variant separator |
#[…] | attribute (compiler-known or procedural macro), outer form |
#![…] | inner attribute, applies to enclosing item |
name!(…) | declarative or procedural macro at expression position |
.. (prefix) | struct-update spread |
_ | wildcard / anonymous binding |
Top / ⊤ and Bot / ⊥ are type-level constants.
Unicode notation
Each operator/keyword below has an equivalent Unicode glyph; the glyph and its ASCII
spelling lex to the same token, so they are fully interchangeable and nothing past the lexer
distinguishes them. Glyphs and the editor \abbrevs follow the Lean lean4-input conventions;
the metric-modal glyphs (⊞/⊟) follow the DatalogMTL literature. The authoritative table is
[[notation]] in compiler/crates/oxc-syntax/grammar.toml, the single source from which the
lexer’s aliases and the editor input methods are generated.
| Glyph | ASCII | \abbrev | Glyph | ASCII | \abbrev | |
|---|---|---|---|---|---|---|
∀ | forall | \forall | ≤ | <= | \le | |
∃ | exists | \exists | ≥ | >= | \ge | |
↔ | iff | \iff | ≠ | != | \ne | |
¬ | not | \not | → | -> | \to | |
∧ | && | \and | ← | :- | \l | |
∨ | || | \or | ∈ | in | \in | |
⊑ | <: | \sqsubseteq | ∪ | union | \cup | |
⊤ | Top | \top | ∩ | intersect | \cap | |
⊥ | Bot | \bot | ∖ | except | \setminus | |
□ | box | \box | ⊞ | box_plus | \boxplus | |
◇ | diamond | \diamond | ⊟ | box_minus | \boxminus |
=> is deliberately not given a Unicode form: it is match-arm / head syntax, not implication,
so ⇒ would mislead. diamond_plus / diamond_minus have no single conventional glyph (the
notation is ◇ with a directional marker) and stay ASCII-only for now.
Boolean operator placement. Argon distinguishes two syntactic positions:
- Rule-body position — after
:-inderive/check, insideunsafe logicblocks, thefrom/whereclauses ofquerybodies, subquery bodies (collect/set/one/count/exists), refinementwhere { … }/iff { … }clauses, andrequire { … }precondition blocks. Conjunction is,; NAF isnot atom. Disjunction is not inline — write multiple rules with the same head, or combine subquery variants viaunion/intersect/except. - Expression position — inside
fnbodies,if/matchconditions,letinitializers, the RHS of comparison atoms inside rule bodies, and atom arguments. Boolean operators are&&,||,!.
The two are non-overlapping: && is not accepted between rule-body atoms; , is not a Boolean operator in expressions.
Literals
INT ::= [0-9]+ ( '_' [0-9]+ )*
REAL ::= INT '.' INT
STRING ::= '"' chars '"'
DATE ::= '#' YYYY '-' MM '-' DD '#'
DATETIME ::= '#' YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z' '#'
BOOL ::= 'true' | 'false'
LIST ::= '[' (expr (',' expr)*)? ','? ']'
RECORD ::= '{' (Ident ':' expr (',' …)*)? ','? '}'
Set/map literals are constructor calls (Set::new(a, b, c), Map::new((k, v), …)); there is no {1, 2, 3} form (Out of scope).
Modules and packages
Structure
Modules take three forms, all equivalent at the symbol table:
- A
.arfile is implicitly a module named for its file stem. - A directory containing
mod.aris a module; sibling.arfiles (and subdirectories containingmod.ar) are submodules of it. - An inline
mod Name { … }declaration introduces a nested module in the current file.
A mod Name; declaration with no body has a single meaning — Rust’s child-module declaration: it asserts that a sibling Name.ar (or Name/mod.ar directory module) exists and attaches its contents under the current namespace as Name. A file does not rename itself: a content-carrying file whose mod Name; resolves to no sibling is refused with OE0715 (audit dc-06 retired the legacy “module-header” reading, where the first mod Name; of a file silently relabeled that file’s own declarations as module Name). To set a module’s name, use the file/directory layout — the project entry is module root, and any other .ar file is the module named for its stem; to declare a child, create the sibling Name.ar; to keep declarations where they are, omit the mod line. Explicit mod declarations are required; the compiler emits OW0710 when a sibling .ar file exists but no mod declaration references it.
Both project and package entry default to the root module file — preferring the Cargo-style src/root.ar, falling back to a flat root.ar at the package root. A prelude.ar is not an entry default: a package’s prelude is the manifest [package].prelude import model (the auto-imported use-tails of RFD 0038), and a prelude.ar file is an ordinary re-export module reached through the root — conventionally pub used at the root and imported by dependents via use pkg::prelude::*. The entry’s directory is the source root under which the package’s .ar files are indexed.
visibility ::= 'pub' | 'pub' '(' 'pkg' ')'
mod-decl ::= 'mod' Ident ';' // file-include
| 'mod' Ident '{' mod-item* '}' // inline
| 'mod' Ident '=' path '(' expr-list ')' // functor alias (Functor modules)
Default visibility is module-private, following Rust: a default-visibility item is visible to the module that declares it and every descendant module. A private item declared at the package root is therefore visible package-wide (every module descends from the root); a private item in mod M is visible to M and M’s submodules, but not to the root or to sibling modules. This governs the privacy check only — name resolution is unchanged: a descendant still names an ancestor’s item through pkg::/super::/use (there is no bare-name inheritance from a parent module). pub exposes to dependents; pub(pkg) limits to the package.
Imports
use ::= 'use' use-tree ';'
use-tree ::= path ('as' Ident)?
| path '::' '*'
| path '::' '{' use-tree (',' use-tree)* ','? '}'
path ::= Ident ('::' Ident)*
Rust-style: single-symbol (use std::math::Nat;), brace-list (use std::math::{Nat, Int};), glob (use ufo_foundational::prelude::*; — a third-party vocabulary package), and aliased (use foo::bar::Sym as Other;) forms.
A plain use is a private import — visible only inside the importing module. A pub use re-exports: the imported items join this module’s public surface and are visible transitively to dependents, which is how a package prelude works (pub use pkg::prelude::*).
Import-resolution limits. Three forms parse but do not resolve: a single-item use M::X where X is re-exported by M rather than declared there (the glob re-export use M::* resolves it; the named form does not); pub use path as Alias (re-export with renaming); and a capitalized Self:: type path (lowercase self:: works). Import through the glob re-export or the original declaring path.
The prelude is a package feature, not a compiler default (RFD 0038). A package’s [package].prelude (an array of use-tails in ox.toml) is auto-prepended to every module of that package; it is empty by default and carries no vocabulary. type/rel are not in it unless the package opts them in (prelude = ["std::core::{type, rel}"], or prelude = ["pkg::prelude::*"] pointing at a prelude.ar of pub use re-exports). A module opts out with #![no_implicit_prelude]. There is no auto-imported std::prelude; what is always in scope is the language substrate (step 3 below), which is not a library prelude.
Manifest
ox.toml, Cargo-flavored:
[package]
name = "lease-story"
version = "0.1.0"
edition = 0
# Optional package entry override. Without this, the default search prefers the
# Cargo-style `src/root.ar` and falls back to the flat package root `root.ar`.
entry = "root.ar"
[dependencies]
# PATH dependencies (RFD 0030). The key is the path root the
# consumer uses (`use ufo::endurant::Object;`) and must equal the dependency
# package's `[package].name`.
ufo = { path = "../ufo" }
[lattice]
# Decidability-tier ceiling (Standpoints and modal operators). A declaration whose classified tier exceeds
# this ladder name is refused at `ox check` / `ox build` (OE1230, RFD 0030 D6).
max_tier = "recursive"
[standpoints]
# optional package-boundary declarations
[schema]
# Optional source root for generated or nested package layouts. Entry paths are
# resolved relative to this root when the nested entry exists.
root = "src"
Tooling resolves a package directory entry in this order: [project].entry, [project].main, [package].main, [package].entry, then the conventional defaults. With no explicit entry, the default search prefers the Cargo-style src/ layout and falls back to the flat package root (so existing packages keep resolving): both [project] and [package] try src/root.ar then root.ar. The first that exists wins, and its directory becomes the source root — a package laid out as src/root.ar roots all its .ar sources under src/ with no config (ox new/ox init scaffold exactly this). An explicit [schema].root overrides the source root. When no entry is found, the error names every path it searched.
Dependencies (RFD 0030). Only dep = { path = "…" } resolves: each path dependency’s package is loaded and its pub surface folded into the consumer’s workspace under the dependency name as a path root (see Name resolution). The registry/VCS surface — a bare version requirement (ufo = "1.0") or the version/git/branch/tag/rev/registry keys — is recognized and refused with OE1240 (Out of scope), never silently ignored. The dependency name must equal the dependency’s published [package].name (OE1241 otherwise); a dependency cycle is OE1242; the same name resolving to two different directories is OE1243. There is no ox.lock for a path-only graph (path deps are unlocked, Cargo precedent); ox.lock is reserved for the registry/git graph. Manifest honesty (RFD 0030 D6): an unrecognized section or key surfaces a Cargo-style OW1240 “unused manifest key” warning rather than being silently dropped, and [lattice].max_tier is enforced against the Standpoints and modal operators classifier (OE1230).
Name resolution
Resolution of a bare identifier proceeds in order:
- Local scope. Definitions in the current module, including the lexical chain of enclosing
modbodies andimplblocks. - Imports + package prelude. Items brought into scope by
usein the current module, and — unless#![no_implicit_prelude]is set — the package’s prelude ([package].prelude, auto-prependeduses; RFD 0038). The prelude is empty by default and carries no vocabulary. - Substrate. The fixed, always-in-scope language primitives — not a library, and not opt-outable: the primordial types (
Nat,Int,Real,Decimal,Money,Date,Time,DateTime,Duration,Bool,String,Top,Bot) and the builtin type forms (Option,Result,Ordering,List,Set,Map,Range,Truth4,Truth4Of,Diagnostic,Severity, …).
A qualified identifier (containing ::) is resolved by walking the path. The leading segment selects a root:
pkg— the current package’s root. This is the sole self-reference anchor: a package never refers to itself by its own name, so internal paths survive a package rename.pkg::a::b::Xnames itemXin modulea::bof this package. (Argon has packages, not crates — there is nocratekeyword.)self— the current module;super— the parent module (repeatable:super::super::X).- A dependency package name declared in
[dependencies]— e.g.ufo::endurant::Object(a path-dependency vocabulary package, RFD 0030). The dependency’spubsurface is folded into this workspace under its name as a path root;ufo::Xanduse ufo::X;resolve into it. The stdlib rootstdis always available (std::math::Nat). - Otherwise the leading segment resolves against the scope chain — a submodule of the current module, or a name brought in by
use.
Subsequent segments resolve within the preceding module’s namespace. An item therefore has a single absolute name viewed from outside the package (mypkg::a::b::X); viewed from inside, the equivalent absolute form is pkg::a::b::X.
Contextual introducer identifiers (type, or vocabulary names like kind, role, phase, …) are ordinary identifiers — recognized by an IDENT IDENT shape at declaration position and resolved via the algorithm above against the pub metatype (concepts) / pub metarel (relations) declarations visible in scope: declared in this package, or imported from a vocabulary package (across a [dependencies] path-dependency boundary, RFD 0030). Nothing is ambient here — not even type/rel: a package brings the no-commitment baseline into scope explicitly, via use std::core::{type, rel} or its [package].prelude (RFD 0038), exactly as it brings any vocabulary (appendix A). The compiler emits OE0101 UnresolvedName for general failures, and OE0605 UnknownMetatype / OE0606 UnknownMetarel specifically when a leading identifier at declaration position fails to resolve — at ox check and ox build; no artifact is emitted. A use statement that does not resolve is itself refused with OE0103 UnresolvedUseImport (with a did-you-mean over the visible namespace, RFD 0030 D5) — a broken import is never silently accepted.
Module extraction (tree-shaking)
When a module imports another, the elaborator extracts a ⊥-locality module (Cuenca-Grau–Horrocks–Kazakov–Sattler 2008): the smallest subset of the imported module’s axioms that stays non-trivial once every concept outside the used signature is reinterpreted as ⊥. Extraction is a fixpoint, linear-time per iteration, terminating in at most |B| iterations.
The guarantee is Σ-scoped conservativity: every entailment about the used signature survives extraction. Argon strengthens the classical result for its own semantics — closed-world conclusions are preserved (CWA-conservativity), ghost individuals reachable only through non-Σ concepts are pulled in (domain-conservative extraction), defeasible rules drag in their defeaters (defeat-aware extraction — a classical extractor is unsound under non-monotonic semantics, Bonatti-Faella-Petrova-Sauro 2022), and extraction composes safely across an import chain. All are mechanized at spec/lean/Argon/Locality/.
Extraction is how the substrate composes decidability tiers across heterogeneous modules (Tier ladder): each extracted module is classified independently, and conservativity guarantees no entailment is lost. Failures emit OE0710 (conservativity cannot be proven under the importer’s world assumption) or OE0711 (defeat-aware extraction cannot close the defeat graph).
Meta-calculus atom
Three declarations: metaxis, metatype, metarel. They make the language ontology-neutral by letting packages declare the concept-introducing keywords.
metaxis
metaxis-decl ::= 'metaxis' Ident 'for' target metaxis-body? ';'
target ::= 'metatype' | 'metarel' | 'individual' | 'macro' | '[' target (',' …)* ']'
metaxis-body ::= '{' enum-body '}' | '=' TypeExpr ('where' refinement)?
enum-body ::= Ident (',' Ident)* // unordered
| Ident ('<' Ident)+ // totally ordered (chain)
pub metaxis rigidity for metatype { anti_rigid < semi_rigid < rigid };
pub metaxis sortality for metatype { sortal, non_sortal };
pub metaxis identity_provision for metatype { provides, inherits };
pub metaxis dependency_kind for metarel { existential, contingent, none };
pub metaxis ordinality for metatype = Nat;
pub metaxis weight for metatype = Real where _ > 0.0;
A metaxis characterizes the meta level: its values are assigned to metatypes (for metatype) and metarels (for metarel), never to a concrete type directly. The targets name the meta-sort the axis applies to — metatype for the concept tier, metarel for the relation tier — not the type / rel concept introducers. (A future for <DeclaredType> target — a type-specific refinement axis bound to one declared type — is left open by the model but not yet specified.)
Cross-axis constraints are check rules at module level, never embedded in the body.
metatype
metatype-decl ::= 'abstract'? 'fixed'? 'metatype' Ident '=' '{' axis-assign (',' …)* ','? '}' ';'
axis-assign ::= Ident ':' value
value ::= Ident | literal
A metatype body is a record of axis values, so each binding reads like a struct field — a single : separates the axis from its value.
pub abstract fixed metatype category = { rigidity: rigid, sortality: non_sortal };
pub fixed metatype kind = { rigidity: rigid, sortality: sortal, identity_provision: provides };
pub fixed metatype subkind = { rigidity: rigid, sortality: sortal, identity_provision: inherits };
pub metatype role = { rigidity: anti_rigid, sortality: sortal };
pub metatype phase = { rigidity: anti_rigid, sortality: sortal };
A metatype name in scope is contextually a concept-introducing keyword: pub kind Person { … }.
Axes are pure user vocabulary. The axis bindings in a metatype body are the declaring package’s own ontology — validated against the declared metaxis domains (RFD 0027 D2), persisted on the wire, and queryable — but the compiler never reads an axis name. No axis assignment changes elaboration, coverage, or mutation semantics; importing a vocabulary cannot alter substrate behavior because its author happened to name an axis rigidity. The metatypes shown above (kind, subkind, role, phase, category) are example declarations of the kind a third-party UFO vocabulary package would ship — they are not language atoms, and no UFO vocabulary ships with Argon or its stdlib. Other vocabularies (BFO, GFO, custom) define their own metatypes with their own axis assignments; the language treats all of them uniformly.
Substrate behavior comes from two ontology-neutral modifiers (RFD 0027 D6) — the operational shadows of the classic meta-property distinctions, with PL precedents rather than ontological commitments:
abstract— no direct instances. Every type the metatype introduces refuses construction (insert T { … }), direct classification (insert iof(x, T)), and seeded assertion (positivepub fact T(x)) withOE0233; subtypes are unaffected, andpub not_fact T(x)(refuting membership) stays legal. Abstract types are exempt from trait impl-coverage obligations (Resolution) — they structure the hierarchy, and every individual is classified through some non-abstract subtype.abstractmay also be declared per-type on a concept declaration (pub abstract type Vehicle { … }, the UML/PL convention); a type introduced by anabstractmetatype is abstract with no override semantics.fixed— classification decided at construction.insert iof/delete iofagainst a type introduced by afixedmetatype refuses withOE0234(mutate); construction itself is unaffected. Dynamic classification is the default — the restriction is the opt-in, the same posture ascheck. Fixed classification is what makes static modal discharge sound (Modal operators); the Kripke-semantics justification (rigid designation = fixed classification) is mechanized inArgon.Reasoning.StaticDischargeand the mutation-gate theorem inArgon.Runtime.ModifierGates.
A vocabulary author writes both layers together — pub abstract fixed metatype category = { sortality: non_sortal, rigidity: rigid }; — the modifiers carry the behavior, the bindings carry the package’s ontology. Misplaced modifiers (fixed on a concept declaration, either modifier on a struct/enum/relation, duplicates) are refused at parse with OE0008.
metarel
metarel-decl ::= 'metarel' Ident '(' endpoint-list ')' metarel-body? ';'
endpoint-list ::= endpoint (',' endpoint)*
endpoint ::= (Ident ':')? Ident // optional name, then metatype name
metarel-body ::= '{' axis-assign (',' …)* ','? '}'
pub metarel mediation(mediator: relator, mediated: kind) {
dependency_kind: existential,
};
pub metarel material(kind, kind) { dependency_kind: contingent };
pub metarel three_way_dependence(kind, kind, kind);
The metarel name is itself a contextual relation-introducing keyword (Relations): pub mediation Involves(m: Marriage, p: Person) declares a relation classified by mediation. The elaborator verifies the relation’s endpoint metatypes match the metarel’s positions. (An alternative decorator-attachment spelling — #[mediation] on a generic relation declaration — was sketched in earlier drafts but is not built: user-declared metarel names are not compiler directives, so the attribute form refuses at the directive registry, OE0705. The introducer form above is the supported surface.)
The stdlib ships a generic metarel for ontology-uncommitted use:
// in std::core — the generic metarel (opt-in via `use std::core::rel`,
// RFD 0038), shown as it is declared inside the stdlib (not a user file).
pub metarel rel<E1: metatype, E2: metatype>(E1, E2);
rel accepts any endpoint metatypes (inferred from the relation’s parameter types). After use std::core::rel;, the modeler can write pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*] for relations without committing to a specific ontological vocabulary. Vocabulary packages may re-export std::core::rel in their prelude as a convenience.
Reflection
Reflective sorts: Entity, TypeRef, Metatype
The substrate exposes three reflective sorts and a fixed lattice over them:
Metatype <: TypeRef <: Entity
Entityis the universal sort of all entities — individuals and type-references alike. A type is an entity (so it can itself be classified by a higher-order type; see Higher-order modeling).Entity <: Top.TypeRefis the sort whose values are references to declared types (concept / construct / relation / metatype) — a handle into the closed, nominal catalog of declarations, in the manner of OWL-2 punning, Java’sClass<?>, or Haskell’sTypeRep.TypeRefis unsealed, soMetatyperemains the only sealed primitive. BecauseTypeRefdenotes a closed catalog of already-declared types rather than an impredicative universe, there is noType : Type: the reflective layer is a predicative, stratified universe of codes, not a self-membership hierarchy.Metatypeis the sealed primitive that classifies all metatypes (kind,role,phase, …, andMetatypeitself). SinceMetatype <: TypeRef, every metatype name in scope is also aTypeRefvalue.
Type-as-value. A declared type’s name in value position is a TypeRef value — Person, kind, and Metatype are all TypeRef values, distinguished from the type Person by syntactic position. This is the substrate’s type-as-value facility: a field or parameter may be typed TypeRef (or the bounded TypeRef<C>; see Built-in type forms) and carry a reference to a declared type. The runtime carrier is the declared-symbol reference Value::Name; on the wire, a TypeRef/Entity argument is a qualified class-path ("pkg::mod::Type", or {"$typeRef": "pkg::mod::Type"}) resolved to the declared type’s identity — entity refs (#i…) are not accepted for these parameters. See RFD 0023.
Reflection intrinsics
The substrate provides five reflection intrinsics — meta, iof, specializes, extent, and implements (RFD 0026; signature implements(t: TypeRef, tr: TraitRef) -> Bool, materialized as the catalog-closed $implements relation with supertrait closure and <: upward target-coverage, Reflective conformance: TraitRef and implements) — admitted in any rule body without a use declaration. The first three have syntactic-sugar atom forms in Rule-atom grammar; extent and implements are called directly. For meta/iof/specializes/extent, each type-position argument is a TypeRef value — a literal type-reference or a bound variable of sort TypeRef — which is precisely what makes the intrinsics first-class, value-polymorphic predicates: library code can pass, count over, or quantify across type-values (see Higher-order modeling and the unprivileged mlt package, realizing the spec/research/RP-003-mlt-as-library.md GAP-1). implements differs in two deliberate ways: its second argument is TraitRef-sorted (traits are not types and stay off the <: lattice), and because $implements is a finite catalog-closed relation, both of its positions may be free — enumeration is the intended use (Reflective conformance: TraitRef and implements).
meta has the signature meta(x: Entity) -> TypeRef: it returns the immediate classifier of x — its <:-minimal declared type — as a TypeRef value. Under multiple classification (an entity instantiating incomparable types, e.g. a UFO individual that is both a Person and a Customer with neither <: the other), meta(x) is multi-valued: it yields one TypeRef per <:-minimal classifier. (It is a Metatype value only when x is itself a type — e.g. meta(kind) == Metatype — since Metatype <: TypeRef.) x :: T is syntactic sugar for meta(x) == T.
meta() applies only to ontologically-classified concepts (those declared under a vocabulary or stdlib metatype). Calling meta() on a struct/enum-declared value is a category error — language-level data carries no metatype — and emits a diagnostic at elaboration.
Worked examples (each right-hand side is a TypeRef value): meta(Person) == type when Person is pub type Person { … } under std::core::type; meta(Person) == kind when Person is pub kind Person { … } under an imported (or locally-declared) UFO vocabulary; meta(kind) == Metatype; meta(Metatype) == Metatype (sealed self-instantiation). Metatype is the sealed primitive that classifies all metatypes; it is special-cased by the compiler. The classifier kind and the primitive Metatype are TypeRef values because Metatype <: TypeRef.
iof has the signature iof(x: Entity, t: TypeRef) -> Bool. The rule-atom form x : T (Rule-atom grammar type test) is syntactic sugar for iof(x, T). Because the type-position argument t is a TypeRef value, iof is a first-class predicate: it can be passed as a relation argument, counted over, or quantified across — for example, library code building higher-order theories (the unprivileged mlt package, Higher-order modeling, Stdlib (selected)) treats iof as a first-class predicate.
iof is read at two tiers. The individual/ABox tier is membership of an order-0 entity in a type, asserted by a pub fact T(x) ground atom. The catalog/type tier is a declared type×type membership — a type X declared an instance of a higher-order type T via the : T instantiation clause (Vocabulary concepts and the generic type metatype, pub kind USD : Currency). The subject x is itself a TypeRef value (a type used as a higher-order individual), so the same iof relation carries both; which tier a read sits in is decided by whether the subject is an individual or a type, not by a separate relation. The catalog tier is declaration-derived (never an ABox event), so a #[static] check (Rule atom — fn, derive, query, mutate, check) may read it and extent projects it at the type level.
specializes has the signature specializes(t1: TypeRef, t2: TypeRef) -> Bool. The rule-atom form t1 <: t2 (Rule-atom grammar specialization) is syntactic sugar for specializes(t1, t2). Same first-class, value-polymorphic motivation as iof — both arguments are TypeRef values.
iof and specializes apply only to ontologically-classified concepts. Calling them on struct/enum-declared values is a category error and emits a diagnostic at elaboration, parallel to the meta() discipline.
extent has the signature extent(t: TypeRef) -> Set<Entity> (t is a TypeRef value naming the type whose extent is enumerated). Returns the set of entities x such that iof(x, t) holds at the current state. Both function-style (extent(EmperorPenguin)) and UFCS method-style (EmperorPenguin.extent()) are admitted; the method form lowers to the function form.
Worked examples: extent(Person) returns all order-0 entities classified as Person; extent(BirdSpecies) returns all order-1 kinds that are iof-instances of BirdSpecies (e.g., {EmperorPenguin, GoldenEagle, Grouse, Pheasant}); extent(Metatype) returns the set of all metatypes in scope. extent() on a struct/enum-declared value is a category error, parallel to the other reflection intrinsics.
Relation-signature reflection (rel / arm). Two further reflection predicates reflect over declared relations (the relation tier of the reflective catalog, RFD 0031 D2), so a vocabulary package can write relation-level compile-time constraints:
rel(r: TypeRef, m: TypeRef) -> Bool—ris a declared relation andmits classifying metarel. One catalog row per declared relation.arm(r: TypeRef, pos: Int, t: TypeRef) -> Bool— for relationr,posis a 0-based arm position andtthe endpoint type declared there. One row per (relation, position).
Both are catalog-closed (one row per declared relation / arm, never the ABox), so — like $implements — every position may be free and enumeration is the intended use. rel/arm are abstract reflection primitives, never ontological vocabulary; the metarel/type names a check matches on are user vocabulary, compared by catalog identity. A package-shipped check quantifies over the catalog to enforce an endpoint discipline:
// "an endpoint of a `characterization` relation must be aspect-sorted"
pub check BadCharacterization(r: TypeRef, t: TypeRef) :-
rel(r, characterization), arm(r, 1, t), meta(t) == kind
=> Diagnostic { severity: Severity::Warning, code: "UFO::W010", message: "..." };
(The metarel introducer itself also verifies its endpoint metatypes at elaboration — metarel, OE0631; $rel/$arm let a package extend or audit that discipline declaratively.)
Lowering. A declared type in value position lowers to the IR primitive Term::TypeRef. iof(x,t) (and x : t) lowers to the Core-IR typeTest atom and meta(x) == T (and x :: T) to the metaEq atom (spec/lean/Argon/CoreIR/RuleIR.lean); specializes(t1,t2)/t1 <: t2 has no dedicated atom and lowers to a reserved-head predicate, and extent(t) to an application of the extent builtin. Beneath Core IR, the reasoner evaluates these against catalog relations it materializes from the declarations — $iof (entity × type, closed under supertypes; this carries both tiers — the individual-tier rows from pub fact T(x) assertions and the catalog-tier type×type rows from each : T instantiation clause, the latter closed upward over the target type’s <: ancestors), $specializes (type × type: the reflexive-transitive specialization graph plus the built-in Metatype <: TypeRef <: Entity edges and the sibling TraitRef <: Entity edge), $meta (entity × its <:-minimal classifiers), $implements (type × trait, supertrait closure and <: upward target-coverage folded in — Reflective conformance: TraitRef and implements), and the relation-tier $rel (relation × metarel) / $arm (relation × position × endpoint type) — RFD 0031 D2. OE0212 MetaArgUnbound is the reserved refusal for an unbound argument of the four type-position intrinsics; implements is exempt by design — both of its positions enumerate over the catalog-closed $implements relation, which is the intended use.
Typical use: deriving cross-level properties of higher-order types from their instances.
#[categorizes(Bird)]
pub type BirdSpecies {
numberOfLivingInstances: Int = extent(self).count(),
averageHeight: Decimal = extent(self).map(|b| b.height).avg(),
}
The = expr field-default form shown above refuses with OE0237 FieldDefaultUnsupported (see struct and enum — language built-ins (data declarations) for the full disposition). Express cross-level derivations such as EmperorPenguin.numberOfLivingInstances with the from-navigation field form ([RFD 0005, struct and enum field navigation](constructs/struct-and-enum.md)) or an explicit derive/query rule (Rule atom — fn, derive, query, mutate, check).
Constructs atom
Concepts and relations are co-equal first-class entities. The construct atom is layered:
- Language built-ins (lexer-level, always available):
structandenum— pure data declarations with no ontological commitment, no metatype classification, structural equality. - Stdlib ontological constructors (opt-in, not ambient — brought into scope via
use std::core::{type, rel}or a[package].preludeentry, RFD 0038): the generic metatypetypeintroduces ontologically-classified node-concepts (pub type Foo { … }); the generic metarelrelintroduces ontologically-classified relations (pub rel R(...)). They are the no-commitment baseline, imported like any vocabulary — nothing is ambient. - Vocabulary classifiers (require a
pub metatype/pub metareldeclaration in scope — declared in this package, or imported from an external vocabulary package): metatypes like UFO’skind,role,phase,categoryand metarels likemediation,materialfor richer ontological commitment. No vocabulary ships with the language or the stdlib — a vocabulary is an ordinary external package (e.g. a UFO package authored by the UFO authors), and an introducer that resolves to no visible declaration is refused withOE0605/OE0606.
All four layers compile to one substrate IR. The distinction lives in the metatype/metarel calculus (Meta-calculus atom); the language itself ships only the calculus, the data-declaration shorthands, and the meta-declaration keywords.
struct and enum — language built-ins (data declarations)
struct-decl ::= attribute* 'pub'? 'struct' Ident generic-params? where-clause? '{' field-list '}'
enum-decl ::= attribute* 'pub'? 'enum' Ident generic-params? where-clause?
'{' variant (',' …)* ','? '}'
field-list ::= field-decl (',' …)* ','?
field-decl ::= attribute* 'mut'? Ident ':' TypeExpr ('=' expr)? ('from' relation-ref)?
variant ::= Ident ( '(' TypeExpr (',' …)* ')' | '{' field-list '}' )?
pub struct Diagnostic {
severity: Severity,
code: String,
message: String,
range: Option<Range>,
}
pub enum Severity { Error, Warning, Info, Hint }
pub enum Option<T> { Some(T), None }
pub enum Result<T, E> { Ok(T), Err(E) }
pub enum Shape { Circle, Square, Triangle }
One enum spelling.
enumhas a single declaration form — the brace list above — matching Rust and the only form that carries payloads. An earlierenum Name = A | B | Cpipe spelling is removed (OE0009). Conceptcoverdeclarations (Vocabulary concepts and the generictypemetatype) take the matching brace formpub kind Name { A, B }(OE0012on the old= A | Bpipe), so no=-pipe surface survives.
struct/enum declarations carry no metatype classification. They are language-level data shapes — structural equality, no fact-store identity beyond storage, no participation in the metatype calculus. They cover stdlib data records (Diagnostic, Severity, Option, Result, Path, Range, Truth4) and user plumbing types. meta() reflection (Reflection) does not apply.
Struct and enum values
A struct value is constructed by naming the type and giving every field: Row { a: 1, b: 2 }. This is an ordinary expression — it can be a let binding, a field value, a function argument, a return value, a list element, or either side of a comparison. The full construction and projection grammar lives in the expression grammar (Expression grammar); this section states the data semantics.
pub struct Point { x: Int, y: Int }
pub fn origin() -> Point = Point { x: 0, y: 0 };
pub fn project_x(p: Point) -> Int = p.x;
Structural equality. Two struct values are equal when their fields are equal, field by field — there is no identity to compare. Field order in the literal does not matter: Row { a: 1, b: 2 } and Row { b: 2, a: 1 } are the same value. Equality is recursive: a struct whose field is itself a struct compares that field structurally too. This is what “pure data” means concretely — a struct is its fields, nothing more.
pub struct Row { a: Int, b: Int }
test "structural equality is field-wise and order-independent" {
assert Row { a: 1, b: 2 } == Row { b: 2, a: 1 };
assert Row { a: 1, b: 2 } != Row { a: 1, b: 3 };
}
Immutability. A struct value is immutable: its fields are fixed at construction and never change. There is no in-place update. To produce a struct that differs from an existing one in a few fields, use functional update — the ..base spread — which yields a new value and leaves the base untouched (Expression grammar):
pub struct Row { a: Int, b: Int }
test "spread is a functional update yielding a new value" {
let base = Row { a: 1, b: 2 };
assert Row { ..base, a: 9 } == Row { a: 9, b: 2 }; // a overridden, b carried
assert base == Row { a: 1, b: 2 }; // base is unchanged
}
Because a struct value is immutable, mut on a struct field is meaningless and is refused — mut field: Type on a struct raises OE0253 MutOnStructField. The mut modifier marks a concept field updatable by an update … set { … } statement inside a mutate body (mutate); that notion belongs to identity-bearing concept individuals, not to value-semantic data.
pub struct Point { mut x: Int, y: Int }
No identity — insert is refused. insert T { … } mints an identity and records an iof classification; it is how concept individuals are born. A struct has no metatype and no identity, so insert on a struct type is refused — OE0252 InsertOfStruct. Construct the struct as a plain value instead (drop the insert).
pub struct Point { x: Int, y: Int }
pub mutate make() {
let p = insert Point { x: 1, y: 2 };
}
A struct literal must state every field exactly. A missing field is OE0249 StructFieldMissing, an undeclared field is OE0250 StructFieldExtra, and a field given the wrong type is OE0251 StructFieldTypeMismatch. These are build-time refusals, named to the field, not deferred to runtime.
Enum values. A payloadless variant is written as a path: Severity::Error, Shape::Circle. A payload variant carries one value and is constructed by applying the variant to that value: Msg::Text("hi"). Enum values compare structurally, the same as structs — Msg::Text("hi") == Msg::Text("hi"), and a payload variant never equals a different variant. A payload variant takes exactly one value; any other count is OE0256 EnumPayloadArity.
pub enum Msg { Text(String), Empty }
pub struct Mail { body: Msg }
pub fn greeting() -> Mail = Mail { body: Msg::Text("hi") };
pub fn blank() -> Mail = Mail { body: Msg::Empty };
User-concept generics are decorative. A user-declared generic
enum Opt<T>(orstruct) parses, but its type parameters reach no storage slot, so applying it —Opt<Int>— is refused (OE0235). The optional and result shapes used in practice are the library genericsOption<T>/Result<T, E>and the optional-field formT?, whose argument shapes the elaborator does interpret. Payload binding for the optional form is the rule-bodyis Some(a)/is Nonetest (next paragraph) or a value-positionmatchexpression (let v = match opt { Some(x) => x, None => d };, below); a payload-binding arm in statement position refuses withOE1319.
Reading an enum payload — is Some(a). Inside a rule body, an optional field’s payload binds with the membership test path is Some(binder), and absence tests with path is None. A present field materializes one row binding the payload; an absent field materializes none, so the surrounding conjunction fails (RFD 0007 Rule-atom grammar):
pub type Person { mut age: Int? }
pub derive Adult(p, a) :- p: Person, p.age is Some(a), a >= 18;
pub derive HasNoAge(p) :- p: Person, p.age is None;
Reading an enum payload — value-position match. A match expression in value position — a let right-hand side, a comparison operand, a fn body, a return/require value — binds an enum payload and evaluates. let r = match opt { Some(x) => x, None => 0 }; reads the payload into x and yields the selected arm’s value; this is the idiomatic way to read or consume an enum value. The arm binds exactly one variable positionally; a zero-, multi-, or record-binder arm is OE0257 EnumPayloadPattern (Expression grammar).
pub enum Opt { Some(Int), None }
test "value-position match binds the payload and evaluates" {
let some = Opt::Some(7);
let none = Opt::None;
assert (match some { Opt::Some(x) => x, Opt::None => 0 }) == 7;
assert (match none { Opt::Some(x) => x, Opt::None => 99 }) == 99;
}
A payload-binding arm in statement position — an effectful arm body inside a mutate statement-match, e.g. match opt { Some(v) => { let _ = insert T { n: v }; }, None => {} } — refuses with OE1319 rather than silently mis-evaluating. Bind the payload in value position (let v = match opt { … };) first, then run the effect. The rule-body is Some(a) form above is the other supported reader; the constant-pattern subset of match (payloadless variants, literals, _) executes in both positions (Pattern matching).
Field defaults. The field-default form field: Type = expr refuses with OE0237 FieldDefaultUnsupported: the parser recognizes the = expr clause and rejects it loudly rather than silently dropping the default. Supply the value at the construction or kind-level binding site, or use the from-navigation form (next paragraph) for a derived value.
Field navigation views — from. Unaffected by the above: a field may carry a from Rel.endpoint navigation-view clause supplying a derived value (RFD 0005, relation subsumption). This form is parsed and elaborated normally; it is the supported way to compute a field’s value from related entities.
Field mutability — mut. A field declaration may carry the mut modifier between any leading attributes and the field name: mut field: Type. Without mut, the field is immutable post-construction — its value is fixed at the construction site (or by #[intrinsic] kind-level binding, or by from-clause derivation) and may not subsequently be re-assigned. With mut, the field admits update-stmt writes inside mutate bodies (mutate).
pub type Person {
name: String, // set at construction; not updatable
dob: Date, // set at construction; not updatable
#[intrinsic] ssn: String, // must be set at kind binding; not updatable
mut current_address: String, // updatable via `update`-stmt
mut current_employer: Company?,
#[intrinsic] mut current_role: Role, // required at construction AND updatable later
}
mut is orthogonal to the other field-decl modifiers: #[intrinsic] governs construction-time required-ness, not updatability (both can apply); a mut on a from-derived field is contradictory and rejected with OE0822 MutOnDerivedField; a default = expr supplies a fallback at construction; the metatype’s fixed modifier (metatype, mutate) governs whether iof classification can change, not whether fields can, so a fixed kind admits mut fields. The modifier reads identically on struct, enum-variant, and vocabulary-concept fields. Mutations lower to append-only retract/assert event pairs on the axiom log (Storage layer); the proposition’s (entity_id, property_id) identity is stable across edits, so bitemporal queries reconstruct prior values (Temporal substrate).
Vocabulary concepts and the generic type metatype
When a vocabulary metatype is in scope, its name introduces a concept:
// A declaration is terminated by exactly ONE of: its `{ }` body, its `{ }`
// cover, or — when it has NEITHER — a REQUIRED ';' (the same item-termination
// rule as Rust: `struct Foo;` vs `struct Foo { … }`). A missing terminator on
// the bodyless form is `OE0011 MissingTerminator`.
concept-decl ::= attribute* 'pub'? <metatype-name> Ident generic-params?
supertype? refinement-clause? ( cover | body | ';' )
supertype ::= specialization? instantiation?
specialization ::= '<:' TypeExpr (',' TypeExpr)*
instantiation ::= ':' TypeExpr (',' TypeExpr)*
cover ::= '{' variant (',' variant)* ','? '}' // bare-ident members; self-terminating
body ::= '{' (field-decl | instance-value) (',' …)* ','? '}' // self-terminating — no ';'
instance-value ::= Ident '=' expr // see body modes below
A cover and a field/instance body share the { … } surface; the member shape distinguishes them. A cover’s members are bare concept names (a brace whose first entry is an Ident followed by , or }), matching enum’s { Circle, Square } and the complete Parent { A, B } group axiom; a body’s first entry carries a : (a field name: Type), :: (a per-target axis override), or = (an instance-value).
The leading identifier (type, or a vocabulary name like kind, subkind, role, phase, category, …) is contextually a concept-introducing keyword — resolved per Name resolution against the pub metatype declarations visible in scope: declared in this package, or imported from a vocabulary package. type is not ambient — it is the std::core baseline, brought into scope by use std::core::{type} or a [package].prelude entry (RFD 0038). An introducer that resolves to no visible pub metatype is refused with OE0605 UnknownMetatype at ox check and ox build.
use std::core::{type}; // the no-commitment baseline, brought into scope (RFD 0038)
pub type Document { title: String, body: String }
// Vocabulary-classified concepts: the vocabulary must be IN SCOPE.
// Here it is declared in-package (the external-vocabulary-package
// pattern — a real UFO package would ship these declarations and be
// imported instead; nothing UFO-related ships with Argon):
pub metatype kind = { };
pub metatype subkind = { };
pub metatype category = { };
pub kind Person { name: String, age: Nat }
pub subkind Adult <: Person iff { self.age >= 18 }; // defined class: auto-classified (Refinement)
pub category Animal;
// Concept with a cover — the fields attach to each variant via subkind hierarchy.
pub kind Vehicle { Car, Truck, Motorcycle }
pub subkind Car <: Vehicle { license_plate: String }
pub subkind Truck <: Vehicle { license_plate: String, cargo_kg: Nat }
pub subkind Motorcycle <: Vehicle { license_plate: String, weight: Real }
Supertype clauses. <: T declares specialization (intra-classification subtyping): Adult <: Person says every adult is a person. : T declares iof-instantiation: pub kind USD : Currency says USD is an instance of the higher-order type Currency. Both clauses are general substrate features — neither commits to any specific higher-order theory. Higher-order theories like MLT (the unprivileged mlt package, Higher-order modeling, Stdlib (selected)) interpret the order arithmetic over :; the substrate itself only enforces that iof is well-founded and acyclic.
Multiple parents on either axis. Both clauses admit a comma-separated list, and both can co-occur on a single declaration:
pub subkind Penguin
<: FlyingAnimal, AquaticAnimal // specializes two kinds at order n
: Vertebrate_Species, Endangered_Species // instance of two categories at order n+1
{
name: String,
body_plan = "tetrapod",
iucn_status = IucnStatus::Vulnerable,
}
When both clauses are present the canonical order is <: then :, mirroring the order arithmetic (specialization parents at the concept’s own order, instantiation parents one above). The order is a style convention, not a rule: both clauses are independent (parents are an unordered set on each axis), so either source order parses and means the same thing — writing : before <: is accepted with the OW0010 style warning (never an error), and oxfmt normalizes it to the canonical order. All parents in a single <: clause must be at the same order; all parents in a single : clause must be at one order above. Unprivileged theory packages (e.g. mlt) enforce additional cross-parent constraints over the iof DAG.
Diamond resolution. When multiple parents (on either axis) declare a field with the same name, the elaborator rejects the declaration with OE0208 AmbiguousFieldFromMultipleParents. The modeler resolves the ambiguity by qualifying the field with its parent:
pub category Vertebrate_Species { habitat: String }
pub category Endangered_Species { habitat: HabitatProtectionZone }
pub subkind Penguin : Vertebrate_Species, Endangered_Species {
Vertebrate_Species::habitat = "Antarctic coast",
Endangered_Species::habitat = HabitatProtectionZone::IUCN_II,
}
Field access from instances follows the same qualification when ambiguous: bar.habitat is rejected with OE0208; the modeler writes bar.Vertebrate_Species::habitat to disambiguate. The substrate offers no automatic merge, override-by-position, or last-wins behavior — every collision is resolved explicitly. (This is consistent with how Argon’s trait system handles method-name collisions; see Trait atom.)
Body modes when : T is present. When a concept is declared as an iof-instance of another (pub kind USD : Currency { … }), the body admits two statement kinds:
field = value— sets the value of a field defined on the targetT. This is the kind-as-instance facet:USDis filling in fields thatCurrencydeclared on its instances.field: Type— declares an instance-level field that the kind itself will carry forward to its own instances. This is the kind-as-kind facet: everyUSDbill will have its ownserial_number.
pub category Currency {
#[intrinsic] iso_code: String,
#[intrinsic] decimal_places: Nat,
symbol: String,
}
pub kind USD : Currency {
iso_code = "USD", // value for the Currency-defined field
decimal_places = 2,
symbol = "$",
serial_number: String, // instance-level field on bills
denomination: Nat,
}
let my_twenty = USD { serial_number: "L12345678A", denomination: 20 };
#[intrinsic] on a field declares it must be set at kind level by every iof-instance. Cross-level binding diagnostics: OE1901 InstantiationFieldNotInTarget (a = binding names a field absent from the target metatype), OE1902 InstantiationFieldShadows (a : field shadows an inherited one), OE1908 IntrinsicPropertyMissing (a required #[intrinsic] field is left unbound), and OE0208 AmbiguousFieldFromMultipleParents (a field declared by multiple parents, unqualified). Theory-specific cross-level constraints (e.g., MLT’s categorization / partition / subordination / powertype rules) are not compiler diagnostics: a higher-order theory ships as an unprivileged package (e.g. mlt, Higher-order modeling) and authors those constraints as its own rules/checks over the neutral substrate; see appendix C.
Field access through the iof chain. For an instance bar: SomeKind where SomeKind : SomeCat and SomeCat declares field x, the access bar.x is resolved by walking up the iof chain:
- Instance fields first. If
barhasxdeclared as an instance-level field (viax: TypeinSomeKind’s body), returnbar’s own value. - Kind-level values next. Otherwise, look at
meta(bar) = SomeKind. IfSomeKindsetx = valueat its kind level, return that value. - Walk further up. If
SomeKinditself does not bindx, continue:meta(meta(bar)), etc., until eitherxis found or the chain bottoms out at an entity with no further:parent. - Not found. Error:
OE0101 UnresolvedName(or a more specific field-not-found variant).
Three equivalent surface forms reach the same value when x lives at the kind level:
| Form | Meaning |
|---|---|
bar.x | auto-walk; returns the first x found going up the iof chain |
meta(bar).x | explicit step to bar’s kind; field access on the kind |
SomeKind.x | direct field access on the type entity itself |
Shadowing across levels. If x is bound at both an instance level and a higher kind level — different types, or the same type with different values — the elaborator rejects the declaration with OE1902 InstantiationFieldShadows. Modelers either rename, or accept the shadow explicitly by writing meta(bar).x / SomeKind.x when they need the kind-level value. There is no implicit override.
Relations
Like concepts, relations are introduced by a vocabulary-defined metarel — there is no built-in rel keyword at lexer level. Stdlib ships a generic metarel std::core::rel<E1, E2>(E1, E2) for ontology-uncommitted relations; vocabulary packages can ship richer metarels (mediation, material, etc.) or re-export the generic.
// Same terminator rule as a concept (Vocabulary concepts and the generic `type` metatype): the `{ }` rel-body self-
// terminates; the bodyless form requires a ';' (`OE0011` if absent).
rel-decl ::= attribute* 'pub'? <metarel-name> Ident '(' rel-param-list ')'
cardinality-list? rel-supertype? ( rel-body | ';' )
rel-supertype ::= ('<:' | 'specializes') TypeExpr (',' TypeExpr)*
rel-param ::= Ident ':' TypeExpr
cardinality-list ::= cardinality+ // one slot per endpoint
cardinality ::= '[' range ']'
range ::= Nat '..' Nat | Nat '..' '*' | Nat '..=' Nat | Nat
// In cardinality position, `..` is INCLUSIVE on both ends
// (UML convention): `[1..1]` means exactly one, `[0..*]`
// means zero or more. Distinct from Rust range semantics.
rel-body ::= '{' field-list '}'
The leading identifier (after pub) is contextually a metarel-introducing keyword, resolved per Name resolution against the pub metarel declarations visible in scope — declared in this package or imported from a vocabulary package; rel is not ambient — the std::core baseline is brought in by use std::core::{rel} or a [package].prelude entry (RFD 0038). An unresolved introducer is refused with OE0606 UnknownMetarel. The parser disambiguates concept-decl (no (...) after the new name) from relation-decl (has (...)).
Three surface forms (using the std::core baseline, brought into scope via use std::core::{type, rel};):
use std::core::{type, rel};
// Form A — anonymous binary field on a concept
pub type Person { children: [Person] }
// Form B — named relation with optional navigation views
pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*];
pub type Person {
parents: [Person] from ParentOf.parent,
children: [Person] from ParentOf.child,
}
// Form C — n-ary relation with intrinsic property body
pub rel Marriage(spouse_a: Person, spouse_b: Person) [0..1] [0..1] {
since: Date,
status: MarriageStatus,
}
pub rel Transaction(seller: Person, buyer: Person, item: Asset) {
amount: Money,
at: DateTime,
}
Cardinality (positional, UML association-end convention). For a relation with endpoints e₁, e₂, …, eₙ, the i-th cardinality slot bounds the number of distinct eᵢ for each fixed combination of all the other endpoints — the UML association-end multiplicity. (Equivalently: hold every endpoint but the i-th fixed, and the slot bounds how many distinct eᵢ may complete the tuple.) Omitted slots default to 0..*. For binary relations the slots read left-to-right; pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*] reads “each child has 1..1 parents (slot 1 bounds the parents per fixed child); each parent has 0..* children (slot 2 bounds the children per fixed parent).” For n-ary relations the same per-position rule holds against the fixed combination of the others; pub rel Transaction(seller, buyer, item) [0..*] [0..*] [0..1] reads “for each (buyer, item) pair, 0..* sellers; for each (seller, item) pair, 0..* buyers; and for each (seller, buyer) pair, 0..1 items.”
Anonymous-field cardinality default. A field declared with the inline anonymous-relation form field: [T] (Form A) synthesizes a binary relation with default cardinality 0..* and Set semantics. To bound: field: [T; n..m]. To opt into ordered list semantics: field: [T; n..m, ordered] (lowers to List<T>). [T; <=1] emits OW2402 suggesting T? (Option<T>) instead.
Anonymous fields are structural shorthand: they do not engage the metarel calculus. No metarel name is attached; the synthesized relation is accessed only via dot-traversal. This is symmetric with struct/enum, which are likewise language-level structural and carry no metatype. Named relations (Forms B and C) require a metarel-introducing keyword in scope.
[T] on struct vs concept. On a struct or enum field, [T] is plain List<T> — no synthesized relation, no closure traversal, no metarel. On an ontologically-classified concept (declared under a metatype), [T] is Form A — a synthesized binary relation supporting closure traversal (alice.children+(b)). The difference is whether the enclosing declaration is data (struct/enum) or ontology (declared under a metatype — type or a vocabulary introducer).
When to upgrade Form A to Form B or C. Promote an anonymous field to a named relation when any of: (a) the relation needs to be classified under a metarel (mediation, material, …) so the substrate can reason over its properties; (b) the relation carries intrinsic data of its own (since: Date, amount: Money); or (c) the relation must be referenced as a first-class predicate from rule bodies (derive ancestor(d, a) :- ParentOf(d, a), …). When none of those apply, Form A is the right choice.
Generic relation-property characteristics. A named relation (rel or metarel) may carry the standard relation-algebra property characteristics (the OWL object-property characteristics) — PL/DB-neutral, describing the shape of the relation’s extent with enforcement, not an ontological commitment. All four are shipped by the std::rel standard-library package and must be brought into scope — use std::rel::{transitive, irreflexive, asymmetric, functional} (nothing is ambient — Name resolution / RFD 0038). #[transitive] is a genuine declarative macro (it expands to the closure derive). #[irreflexive] and #[asymmetric] are genuine procedural macros (RFD 0040): they re-emit the relation and paste the guarding check’s internal __{rel}_{prop} head via concat_idents. They read the relation’s name, so they apply to a rel; on a metarel they decline (the invocation is refused, OE0723). #[functional] is a builtin-backed macro (the #[rustc_builtin_macro] analogue, RFD 0037 D8) — the library owns the surface while a privileged compiler path implements it (a [0..1] cardinality cap on a rel’s target, or a guarding check on a metarel); re-emitting a rel with a modified cardinality bracket needs structural reflection. A characteristic applied without its import is refused — OE0705 for the macros #[transitive]/#[irreflexive]/#[asymmetric], OE1362 for the builtin #[functional]. Multiple characteristics compose on one declaration.
| Characteristic | Meaning | Enforcement |
|---|---|---|
#[transitive] (std::rel macro) | the relation is transitively closed | expands to a closure rule R(x, z) :- R(x, y), R(y, z) over the relation’s own extent, so the relation’s queryable extent includes its transitive closure |
#[irreflexive] | no element relates to itself | a check fires on any self-loop R(x, x) (OE1359) |
#[asymmetric] | R(x, y) forbids R(y, x) | a check fires on any mutual pair (OE1360); since a self-loop is its own converse, #[asymmetric] also forbids R(x, x) |
#[functional] | each source maps to at most one target | a max-cardinality [0..1] cap on the relation’s target position, refused at the write path (OE1341) — the same mechanism as a hand-written [0..1] bracket |
use std::rel::{transitive, irreflexive, asymmetric, functional};
#[transitive]
#[irreflexive]
#[asymmetric]
pub rel is_proper_part_of(part: Top, whole: Top);
#[functional]
pub rel inheres_in(burden: Aspect, bearer: ConcreteIndividual); // each aspect inheres in one bearer
Enforcement applies to the declared relation’s own extent. Propagation of a property borne by a metarel to the relations it classifies is out of scope (Out of scope). A characteristic applied without importing it from std::rel is refused — OE0705 for the macros #[transitive]/#[irreflexive]/#[asymmetric] (genuine macros, not directives), OE1362 for the builtin #[functional]. On a non-relation declaration (a concept, a rule), the still-directive #[functional] is refused with OE0707 (invalid position); the procedural #[irreflexive]/#[asymmetric] instead decline (the invocation is refused, OE0723), as they do on a metarel. (The ontology-specific relation properties — MLT’s #[partitions], #[categorizes], etc. — are vocabulary decorators and live in their packages, distinct from this generic family.)
impl Type { … } — grouping
impl-block ::= 'impl' generic-params? TypeExpr '{' impl-item* '}' // grouping
| 'impl' generic-params? TypeExpr 'for' TypeExpr '{' trait-impl-item* '}' // trait impl
The grouping form admits any declaration kind (impl-item); the trait-impl form is
restricted to the contract member forms (trait-impl-item: fn / mutate / derive / check /
query) and is governed by the conformance obligations of
the trait surface and conformance (RFD 0026).
impl Person {
pub fn greeting(self) -> String = "Hello, " + self.name; // instance method
pub fn adult(p: Person) -> Bool = p.age >= 18; // associated function
pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*]; // lexically grouped
pub query oldest() -> Option<Person> {
select p from p: Person order by p.age desc limit 1
}
}
An impl Type block accepts any declaration kind: methods, associated functions, relations, queries, mutations, checks, associated type / const. All declarations inside the block are namespaced under Type::. Methods (items with self first parameter) are additionally accessible via UFCS dot-notation (alice.greeting()); associated functions and relations are reachable only as Type::name(args).
impl Person {
/// Instance method (has `self`): both `alice.greeting()` and `Person::greeting(alice)` work.
pub fn greeting(self) -> String = "Hello, " + self.name;
/// Associated function (no `self`): reached as `Person::adult(p)`.
pub fn adult(p: Person) -> Bool = p.age >= 18;
/// Type-associated relation: reached as `Person::ParentOf(a, b)`.
pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*];
/// Method-shaped query: `alice.oldest()` and `Person::oldest(alice)` both work.
pub query oldest() -> Option<Person> {
select p from p: Person order by p.age desc limit 1
}
}
A relation declared inside impl Type is not exposed at module level. Choose the placement (module level, inside a topic mod, or inside impl Type) according to the semantic association the modeler intends — see Connecting concepts and relations.
Connecting concepts and relations
A relation is first-class, but where it is declared expresses its semantic association with the participating concepts. Three placements coexist; the modeler picks by intent:
- Module level — a free predicate owned by no single endpoint:
pub rel ConnectedTo(a: Node, b: Node) [0..*] [0..*];, accessed asConnectedTo(x, y)anywhere in the module. - Topic
mod— grouped with related concepts; accessed asfamily::ParentOf(a, b)afteruse family::*;. impl Type(impl Type { … }— grouping) — conceptually “about” the type, alongside its methods; accessed asPerson::ParentOf(a, b)only, not at module level.
Navigation from a concept value to its participations. Three mechanisms link an instance to the relations it participates in.
1. Field-form navigation view (cheapest, declarative):
pub type Person {
parents: [Person] from ParentOf.parent, // alice.parents
children: [Person] from ParentOf.child, // alice.children
}
Each field is a derived view over the relation. The compiler verifies the field type matches the role’s endpoint type and that cardinality is consistent with the relation’s per-endpoint cardinality. The view materializes against the relation’s closed extent: because relation subsumption (Relations) makes a subsumed relation’s tuples tuples of its supersuming relation, a view over Rel.endpoint includes the endpoints contributed by every <:-subsumed relation — so a subtype that refines an inherited collection by projecting from a subsumed relation (SatisfactionAccount.records from satisfactionAccountRecords <: recordAccountRecords) sees exactly its narrower elements.
2. UFCS method via impl (richer navigation):
impl Person {
pub query siblings(self) -> [Person] {
select s from ParentOf(p, self), ParentOf(p, s) where s != self
}
}
Methods are appropriate when navigation requires filtering, joining, ordering, or aggregation beyond a simple projection. Accessed as alice.siblings().
3. Direct rule-body invocation (always available):
pub derive grandparent(g: Person, c: Person) :- ParentOf(g, p), ParentOf(p, c);
Inside any rule body, the relation is a predicate addressable by name (or by qualified path: family::ParentOf(...), Person::ParentOf(...)).
Inverse declaration (when both sides are field-form):
pub type Country { citizens: [Citizen] }
pub type Citizen {
#[inverse(Country.citizens)]
countries: [Country],
}
The compiler verifies cardinality consistency and exposes both navigation directions over a single synthesized relation.
Naming convention. Relation names are CamelCase (per Identifiers) — ParentOf, Marriage, IsCitizenOf — symmetric with the CamelCase of metatype-introduced concepts. The name plus argument order encodes semantic direction; ParentOf(p, c) reads “p is the parent of c.” The convention is the modeler’s responsibility — the language does not enforce it. Use field-form views or methods to expose semantically-named navigation in the opposite direction.
Three import postures
A modeler picks an ontological posture per module by choosing what to use. Three cover almost all code:
// Posture 1 — pure data, no ontology. `struct`/`enum` are lexer built-ins.
use std::math::Nat;
pub struct Receipt { lease_id: Nat, paid: Money, at: Date }
pub enum Severity { Error, Warning, Info, Hint }
// Posture 2 — generic ontological commitment via the std::core
// baseline, brought into scope explicitly (RFD 0038).
use std::core::{type, rel};
pub type Document { title: String, body: String }
pub rel Cites(citer: Document, cited: Document) [0..*] [0..*];
// Posture 3 — vocabulary-classified. The vocabulary is an ordinary
// EXTERNAL package — here a hypothetical third-party UFO package;
// nothing UFO-related ships with Argon or its stdlib — or the module
// declares its own `pub metatype` / `pub metarel` vocabulary locally.
use ufo_foundational::prelude::*; // a third-party vocabulary package
pub kind Person { name: String, age: Nat }
pub rel ParentOf(parent: Person, child: Person) [1..1] [0..*];
pub mediation Involves(m: Marriage, p: Person);
Postures compose and are per-module, not per-package: a module may use the imported std::core baseline (use std::core::{type, rel}, or via the package prelude) alongside another imported (or locally-declared) vocabulary, and a package may ship modules in different postures behind one prelude. The metatype calculus (Meta-calculus atom) distinguishes the declarations by the keyword in declaration position.
Group axioms — disjoint, complete, partition
A group axiom constrains a set of sibling concepts collectively: that they never overlap, that they jointly exhaust a parent, or both. The three forms share one declaration shape — a contextual keyword, an optional parent, and a brace list of member concept paths:
disjoint { Cat, Dog, Fish } // pairwise non-overlapping
complete Animal { Cat, Dog, Fish } // jointly exhaustive over Animal
partition Animal { Cat, Dog, Fish } // disjoint AND complete
The keywords (disjoint / complete / partition) are contextual — they lex as ordinary identifiers and bind as group-axiom keywords only in declaration position followed by the group-axiom shape, so they remain available as concept and field names elsewhere. A group axiom introduces no name of its own; it names existing concepts.
disjoint { A, B, C } asserts the members are pairwise non-overlapping: no individual is an instance of two of them at once. Disjointness is instance-level. A write that would classify one individual into two declared-disjoint members is refused (OE0240), so the contradictory state never enters the store. disjoint takes no parent — the members need not share a supertype.
complete Parent { A, B, C } asserts the members jointly cover Parent: every Parent instance is an instance of at least one member. Under the closed-world default (Type system), a Parent instance derivable into none of the members is an uncovered instance, and the write that creates it is refused (OE0241). Each member must be a subtype (<:, transitively) of Parent, checked at elaboration (OE0243 otherwise). Completeness has a second, static half: if the schema declares an instantiable (non-abstract) concept S <: Parent that specializes none of the members, the cover is provably incomplete and the build is refused (OE0242) — an abstract subtype, having no direct instances, is exempt. This static gate ranges only over the concepts declared in the same file as the group axiom; a subtype introduced in another module is not seen at this point, so its covering is left to the runtime OE0241 guard rather than refused statically.
partition Parent { A, B, C } is the conjunction: the members are both disjoint and complete over Parent. It imposes the OE0240 overlap guard, the OE0241 covering guard, and the OE0242 / OE0243 static gates together.
Disjointness and covering are enforced as ordinary substrate checks (Rule atom — fn, derive, query, mutate, check) the compiler synthesizes from the declaration — one overlap check per unordered member pair, one negation-as-failure covering check per cover — so a violating write is refused by the same delta-guard that enforces a hand-written check, with no separate axiom machinery. The covering check is closed-world: it fires when no member membership is derivable. Its interaction with a concept’s #[world(open)] assumption is specified in World assumptions (CWA / OWA) (RFD 0045).
A degenerate group axiom synthesizes no runtime check, because it has nothing to guard. An empty disjoint { } and a one-member disjoint { A } have no unordered pair to overlap, so no overlap check is produced and the declaration is accepted in silence. An empty complete Parent { } likewise produces no covering check — but it still meets the static cover gate above, so any instantiable subtype of Parent (including Parent itself, if non-abstract) is then provably uncovered and the build is refused (OE0242).
Types and refinement
Generics
Angle brackets, Rust-aligned:
generic-params ::= '<' generic-param (',' …)* '>'
generic-param ::= Ident ('<:' TypeExpr ('+' TypeExpr)*)?
Type arguments at call site use turbofish for disambiguation: sort::<Money>(prices).
Subtyping
A <: B declares A is a subtype of B. Two universal bounds frame the lattice: A <: Top and Bot <: A hold for every A.
Beyond the declared <: graph, the substrate has three built-in subtyping sources (oxc-check/src/infer.rs::types_compatible):
- Numeric tower:
Nat <: Int <: RealandNat <: Int <: Decimal.Moneyis not in the tower — there is no implicitInt → Money;Moneyonly arises through the result-side widening of monetary arithmetic (Numeric tower and coercion),Money ▷ Decimal ▷ Real ▷ Int. - Covariant collections:
List<A> <: List<B>andOption<A> <: Option<B>iffA <: B(written[A]/A?). - Invariant elsewhere: tuples,
fn(…) -> _, and generic applications subtype only by equality.
Real, Decimal, and Money are exact arbitrary-precision rationals (BigRational); source numeric literals parse to exact rationals with no f64 round-trip, so 0.1 + 0.2 == 0.3 (RFD 0016, oxc-reasoning/src/compile/value.rs).
Refinement
refinement-clause ::= ('where' | 'iff') '{' refinement-pred (',' …)* '}'
refinement-pred ::= D1Pred | D2Pred
A refinement attaches a predicate to a concept. The keyword chooses how the predicate relates to membership — the description-logic split between a primitive class (⊑, necessary conditions only) and a defined class (≡, necessary and sufficient conditions). See RFD 0017.
where { P }— primitive.Pis a necessary condition: every member satisfies it, but a supertype instance that merely satisfiesPis not thereby a member. Membership is asserted (by construction orinsert iof,mutate);Pis enforced as an invariant at each membership write and never widens the extent. This is Rust’swherebound lifted to a concept’s members — a constraint, not a definition.iff { P }— defined.Pis necessary and sufficient: an instance of the supertype is a member if and only if it satisfiesP. Membership is derived — the substrate auto-classifies and rejects manualinsert iof(mutate,OE0211). Reads as the membership biconditional: “anAdultis aPersoniffage >= 18.”
pub type Adult <: Person iff { self.age >= 18 && self.has_id }; // defined: any qualifying Person is automatically an Adult
pub type ExtinctSpecies <: Species iff { self.living_count == 0 }; // defined: classification derived from state
pub type VettedVendor <: Vendor where { self.risk_score <= 30 }; // primitive: vetting is conferred; the score is a necessary invariant on members
D1Pred (substrate-locked): metaEq, isDet, hasAnc, hasDesc, countGeq, forallC, existsC, plus Booleans. D2Pred: QF-LIA, GNFO, enum equality. The elaborator stages D1 ahead of D2 residual.
Mechanically: D1Pred / D2Pred / MixedPred are inductives at spec/lean/Argon/Decidability/Fragment.lean; the staging theorem stage_correct lives at CrossDomain/Staging.lean; the polynomial bound on D1 evaluation (d1EvalCost n φ ≤ d1Size φ · (n+1)^(d1QuantifierDepth φ)) is proven in Complexity/Bounds.lean. The bound grounds the polytime claim Tier ladder makes for tier:structural and tier:closure.
Three-valued membership under OWA. When the governing world assumption is open (World assumptions (CWA / OWA)), iff-derived membership is three-valued: success requires positive evidence that the predicate holds. A value whose predicate evaluates to unknown does not satisfy the refinement — matching SHACL’s sh:Violation discipline and SQL’s three-valued NULL semantics. Under CWA, unknown collapses to false. The mechanization at TypeSystem/Soundness/CwaOwa.lean proves that CWA-true refinement results survive transition to OWA (monotonic knowledge transfer). The symmetric rule for a primitive where invariant: a membership write is rejected only on positive evidence of violation (P evaluates to a definite false); unknown permits the write (mutate, OE0668). unknown means information absence — a referenced field with no recorded value. A predicate that cannot be evaluated at all (an unsupported form, malformed stored data, or a type-mismatched comparison such as ordering a Date against an Int) is not unknown: unsupported forms are refused at build time (OE0660), and anything that slips past the gate fails the operation loudly at runtime — never a silent permit (where) or a silently-empty derived extent (iff). Refinement predicates evaluate on the same exact value tower as every other value position (RFD 0016): exact rationals for Real/Decimal/Money, chronological Date comparison, with no value-dependent enforcement.
Built-in type forms
| Form | Meaning |
|---|---|
T? | Option<T> |
[T] | List<T> |
[T; n..m] | refined list with cardinality |
Set<T>, Map<K, V>, Range<T> | stdlib generics |
Result<T, E> | error-carrying result |
fn(T1, T2) -> U | function-type expression |
Path<NodeT, EdgeT> | first-class alternating node-edge sequence (The Path type) |
TypeRef, TypeRef<C>, Entity | reflective sorts (Reflection) — values are type-references |
First-class function values include references to declared fns and closures (Expression grammar); both share the fn(T1, T2) -> U type. Higher-kinded types and runtime trait objects (dyn Trait) are out of scope (Out of scope).
Reflective sorts. Entity, TypeRef, and Metatype are the reflective sorts of the meta-calculus (Reflection), ordered Metatype <: TypeRef <: Entity (and Entity <: Top). These are distinct from Top (Subtyping): Top is the <:-greatest type, whereas TypeRef is the sort whose values are references to declared types. The bounded form TypeRef<C> is sugar for the Refinement refinement { t: TypeRef where specializes(t, C) } (RFD 0017, where = primitive/asserted), with TypeRef == TypeRef<Top>. It is covariant — TypeRef<A> <: TypeRef<B> iff A <: B — which follows directly from the refinement ({t | t <: A} ⊆ {t | t <: B}), adding no new structural subtyping rule. Membership is three-valued under OWA (World assumptions (CWA / OWA), RFD 0007): specializes(t, C) unknown ⇒ not a member. Powertype / order / categorization semantics stay in the unprivileged mlt package (Higher-order modeling); TypeRef<C> carries none of them. See RFD 0023.
? propagation
expr? on a Result<T, E>-typed expression unwraps to T on Ok or returns the Err from the enclosing rule. Admitted in fn and mutate bodies, where the enclosing rule’s return type is Result<U, E> for some U. Not admitted in derive (which has no return type — predicates do not fail) or query (whose return type is the projection type, not a Result); check rules emit diagnostics rather than returning Results. Calling ? outside an admitted context is a parse-time error.
Expression grammar
expr ::= primary-postfix
((arith-op | comp-op) primary-postfix)*
(('not')? 'in' primary-postfix)?
('&&' expr | '||' expr)*
primary-postfix ::= unary-op* primary postfix*
unary-op ::= '!' | '-'
postfix ::= '.' Ident '(' expr-list ')' // n-arg method call
| '.' Ident // field access OR zero-arg method call
| '[' expr ']' // index
| '[' expr? '..' expr? ']' // slice
| '?' // try (Result propagation)
primary ::= literal
| '_' // wildcard / anonymous binding (rule-atom arg positions only)
| '(' expr ')'
| list-or-comprehension
| if-expr
| match-expr
| block-expr
| meta-call
| aggregate
| subquery // `query` (count/set/one/collect/exists braces)
| call-expr
| path
| struct-literal
| closure
literal ::= INT | REAL | STRING | DATE | DATETIME | BOOL
list-or-comprehension
::= '[' ']'
| '[' expr (',' expr)* ','? ']'
| '[' expr 'for' Ident 'in' expr ('where' expr)? ']'
if-expr ::= 'if' expr block ('else' (block | if-expr))?
match-expr ::= 'match' expr '{' arm (',' arm)* ','? '}'
block-expr ::= '{' stmt* expr? '}'
block ::= '{' stmt* expr? '}'
call-expr ::= path '(' expr-list ')'
meta-call ::= 'meta' '(' expr ')'
aggregate ::= ('sum'|'count'|'min'|'max'|'avg')
'(' expr ('for' Ident 'in' expr
('where' expr)? (',' rule-atom)*)? ')' // RFD 0029: trailing atoms refine the fold
struct-literal ::= path '{' (struct-init-list)? '}'
struct-init-list ::= field-init (',' field-init)* ','?
| '..' expr (',' field-init)* ','? // struct-update spread
field-init ::= Ident (':' expr)? // `name` shorthand or `name: expr`
closure ::= '|' closure-params? '|' ('->' TypeExpr)? closure-body
closure-params ::= closure-param (',' closure-param)* ','?
closure-param ::= Ident (':' TypeExpr)?
closure-body ::= expr | block
path ::= Ident ('::' Ident)*
expr-list ::= (expr (',' expr)*)?
arith-op ::= '+' | '-' | '*' | '/' | '%'
comp-op ::= '==' | '!=' | '<' | '<=' | '>' | '>=' // non-chainable
Precedence (tightest to loosest):
- Postfix (
.f(),[i],[a..b],.field,?) - Unary prefix (
!,-) - Multiplicative (
*,/,%) — left-associative - Additive (
+,-) — left-associative - Comparison (
==,!=,<,<=,>,>=) — non-associative (a < b < cis rejected; usea < b && b < c) - Membership (
in,not in) — non-associative - Logical AND (
&&) — left-associative, short-circuit - Logical OR (
||) — left-associative, short-circuit
Turbofish disambiguates generic call sites at expression position: sort::<Money>(prices).
Rounding builtins (RFD 0029). A small family of rounding functions is available in expression position — both in rule-body bindings and in the fn/compute plane — operating exactly over the numeric tower (Decimal stays Decimal, never via f64):
| Builtin | Meaning |
|---|---|
round(x) | nearest integer, ties away from zero |
round(x, n) | nearest multiple of 10⁻ⁿ, ties away from zero |
round_half_even(x, n) | nearest multiple of 10⁻ⁿ, ties to even (banker’s rounding) |
trunc(x, n) | toward zero at 10⁻ⁿ |
round_half_even is the money default: financial rounding rounds half to even to avoid the systematic upward bias of half-away-from-zero. All four return an exact value (integral results collapse to Int). A wrong argument count is refused at compile with OE1333 RoundingBuiltinArity.
Closures capture environment by value (Argon is value-semantic; no references). A closure’s type is fn(T1, T2) -> U — interchangeable with declared fn references at any function-typed argument or binding.
Property-style accessors. The paren-less postfix .Ident resolves to a stored field if the receiver type declares one with that name; otherwise to a call of a zero-argument method with that name. This unifies the .field and .zero_arg_method() forms — both read obj.name. The 30.days, 5.minutes, 60.seconds shorthand in Numeric tower and coercion is exactly this affordance over zero-arg methods on numeric types. Methods with one or more arguments always require explicit parens.
Struct-literal evaluation. A struct-literal T { … } evaluates each field initializer and assembles a struct value of type T (the data semantics — structural equality, immutability, no identity — are stated in struct and enum — language built-ins (data declarations)). The literal must state every field: a missing field is OE0249, an undeclared one is OE0250, and a field given the wrong type is OE0251. The field shorthand in field-init (a bare Ident with no : expr) takes the field’s value from a same-named binding in scope.
Functional update — the ..base spread. The ..expr form in a struct-init-list constructs a new struct value from an existing one: it evaluates base, copies every field, then overrides the fields the literal restates. Row { ..base, a: 9 } is base with a replaced by 9 and every other field carried over. The result is a fresh value; base is never modified (struct values are immutable, struct and enum — language built-ins (data declarations)). The spread supplies the fields the literal omits, so the every-field rule is satisfied by the base — a spread literal need only name the fields it changes.
pub struct Row { a: Int, b: Int }
test "spread overrides one field and carries the rest" {
let base = Row { a: 1, b: 2 };
assert Row { ..base, a: 9 } == Row { a: 9, b: 2 };
assert base == Row { a: 1, b: 2 }; // base unchanged
}
Field projection. The postfix .f reads field f off a struct value, by structure. Projection nests — outer.inner.v reads v off the inner struct off outer. A field the struct’s type does not declare is OE0254 StructFieldAccessUnknown.
pub struct Inner { v: Int }
pub struct Outer { inner: Inner, tag: String }
test "projection reads through nested struct values" {
let o = Outer { inner: Inner { v: 7 }, tag: "x" };
assert o.inner.v == 7;
assert o.inner == Inner { v: 7 };
}
Enum payload binding in match. The constant-pattern subset of match — payloadless enum constants, literals, or-patterns of those, and _ — executes (Pattern matching). A payload-binding arm (match opt { Some(x) => …x… }) destructures one payload value into one fresh variable, bound over the arm body. In value position — a let right-hand side, a comparison operand, a fn body, a return/require value — this binds and evaluates: let r = match opt { Some(x) => x, None => 0 }; is the idiomatic way to read an enum payload. The arm binds exactly one variable positionally; a zero-, multi-, or record-binder arm is OE0257 EnumPayloadPattern. A payload-binding arm in statement position (an effectful arm inside a mutate statement-match) refuses with OE1319 rather than mis-evaluating; bind the payload in value position first. The rule-body membership test path is Some(binder) / path is None is the other supported reader (struct and enum — language built-ins (data declarations), RFD 0007 Rule-atom grammar).
Functor modules
A module declaration may take type or value parameters, producing a functor module — a module-valued function:
functor-mod ::= 'mod' Ident generic-params '{' mod-item* '}'
// generic functor
| 'mod' Ident '(' param-list ')' '{' mod-item* '}'
// value-parameter functor
mod-alias ::= 'mod' Ident '=' path '(' expr-list ')' ';' // instantiation
Functor modules are instantiated via the mod Name = path(args); alias form introduced in Structure; the body is re-elaborated per instantiation site with the parameters bound. The substrate guarantees monomorphization at elaboration — there is no runtime module dispatch.
mod TaxJurisdiction<J: Jurisdiction> {
pub type TaxableIncome <: Money;
pub derive owes_tax(p: Person) :- p.income: TaxableIncome, p.income > 0;
}
mod us_federal = TaxJurisdiction<USFederal>;
mod california = TaxJurisdiction<California>;
Parametric modules, not parametric concepts. Argon admits parameterization only at the module level. There is no parametric pub type Container<T> { … } form — a metatype-classified concept is always a closed, monomorphic classifier. When a modeler wants Container<Apple> vs Container<Orange>, the answer is either (a) a specialization hierarchy (pub type AppleBox <: Container) when the variation is ontological, or (b) a functor module whose body declares the per-instantiation concepts when the variation is parametric. This restriction keeps the metatype calculus first-order and the inference tier classifier decidable; lifting it would require admitting kind-polymorphism into refinement and standpoint federation, both of which are unsound at present without further machinery.
A functor body admits any module-level item: concepts, relations, rules, traits, macros, sub-mods, and other functor modules. Each instantiation produces a fully elaborated namespace; symbols from one instantiation do not unify with symbols from another (us_federal::TaxableIncome and california::TaxableIncome are distinct types, both <: Money).
Functor modules are the canonical mechanism for vocabulary-parameterized libraries (per D-007).
The Path type
Path<NodeT, EdgeT> is the first-class type produced by path-projecting query forms (query). It appears in the built-in type forms (Built-in type forms) because the compiler synthesizes path values from query results; the value-level API lives in the stdlib (std::path).
A path is an ordered, alternating sequence n₀, e₁, n₁, e₂, n₂, …, eₘ, nₘ where m = length(path) is the number of edges. A path of length zero is a single node. NodeT and EdgeT are the least common supertypes of the node and edge values respectively.
Edge values by relation form. Edge values vary with which relation form (Relations) the path traverses:
- Forms B and C — named relations. Each edge value is the relation tuple-entity itself, carrying any intrinsic properties:
path.nth_edge(0)?.distanceretrieves thedistancefield of the first edge when the underlying relation declares it. - Form A — anonymous fields. Each edge value is the structural placeholder
Edge<From, To> { from: From, to: To }. Form A carries no metarel classification and no intrinsic data, so the placeholder exposes only endpoint identities.
Heterogeneous chains. When a path traverses relations with differing endpoint types (e.g., user.knows(friend).works_at(company) where knows: User → Person and works_at: Person → Company), NodeT is the join (least common supertype) of User, Person, and Company, and EdgeT the join of the involved relation types. When no common supertype is in scope, both fall back to Top. Modelers needing precise heterogeneous typing decompose into single-relation queries and join at the modeler level.
Hyperedges. Paths are over binary relations only. n-ary relations (pub rel Transaction(seller, buyer, item)) are queryable but not path-traversable; hypergraph traversal semantics is out of scope (Out of scope).
API. The shape/projection/fold operators (length, nodes, edges, nth_edge, reverse, concat, map_*, …) are stdlib API on impl<N, E> Path<N, E> in std::path — see Stdlib (selected). The type-system-relevant facts: cost(self) -> Real lives in a conditional impl gated on E <: Weighted (the Rust-aligned conditional-impl pattern), so a uniform Path<N, E> admits cost() only when the edge type implements the std::path trait Weighted { fn weight(self) -> Real; }; without it, shortest-path queries rank by edge count. The Form-A edge value is the placeholder pub struct Edge<A, B> { from: A, to: B } — no metarel classification, just a first-class edge for uniform iteration.
Equality. Path<N, E> has structural equality: two paths are equal iff their node and edge sequences match position-by-position. Paths are value-semantic and immutable; all transformations produce new path values.
Temporal substrate
Why bitemporal
The substrate before this section operates on a single state mapping concepts to extents. The mutation grammar (mutate) advances the state from snapshot to snapshot; prior snapshots are not addressable. This section introduces the bitemporal substrate: every fact carries a valid time and a transaction time. Bare queries default to the current snapshot; explicit temporal operators address the past, the future, and the historical record of the system’s beliefs.
The formalism is DatalogMTL (Wałęga et al., IJCAI 2019) with stratified negation (Tena Cucala et al., AAAI 2021) over the integer timeline; the reference reasoner is MeTeoR (Wałęga et al., AAAI 2022).
Valid time and transaction time
Two interval annotations on every fact (and every relation tuple):
- Valid time (VT) — when the fact holds in the world.
[VT_start, VT_end], with open-ended[VT_start, ∞]for ongoing. - Transaction time (TT) — when the system recorded the fact.
[TT_start, TT_end], with open-ended[TT_start, ∞]for current belief.
The snapshot view is the projection ${,\mathsf{iof}(x, T),\mid,VT_{\text{start}} \le \mathit{now} \le VT_{\text{end}} ;\wedge; TT_{\text{start}} \le \mathit{now} \le TT_{\text{end}},}$. Bare rule atoms and bare queries default to this view.
The bitemporal extension applies uniformly to all relations, not just iof. A pub rel Enrolment(s: Person, u: University) tuple carries VT and TT just like an iof fact.
Mechanized at spec/lean/Argon/Reasoning/Temporal.lean (BiState as the bitemporal State; TTHistory and TTMonotone for the transaction-time invariant). The bitemporal extension is additive — the termination machinery of Argon.Reasoning.State lifts pointwise without modification.
Retroactive correction and audit
Mutations that close a VT interval retroactively (delete iof(x, T) at #PAST-DATE#) do not erase the prior record. They close its TT interval at now and append a new record with the corrected VT. Audit queries as of TT = #PAST-DATE# reconstruct what the system believed at any past point. The substrate is append-only by default; explicit erasure (forget), crypto-shredding (#[shred_on_forget]), and #[retention(…)] bounds are storage-layer concerns specified in mutate and Storage layer.
Three-valued evaluation under OWA
Under open-world assumption, temporal atoms evaluate to Is | Not | Can. The since and until operators (see Temporal rule atoms) lift to three-valued via the strong-Kleene meet/join tables that truth values defines, composed pointwise across the interval. Unknown temporal extent projects to Can, never to Both — there are no conflicting witnesses, just single-source uncertainty.
Temporal sub-tier
Temporal expressiveness is an orthogonal sub-tier on top of the main tier ladder. Every program has a tier pair (main, temporal) — see Tier ladder.
Rule atom — fn, derive, query, mutate, check
Argon distinguishes the definition of derived predicates (derive) from inference tasks over the resulting theory (query). derive populates IDB predicates that other rule modes can join against, contributing to the inductive closure. query consumes them and returns a typed value with no IDB side-effect. The split mirrors IDP’s { … } inductive-definition block vs. propagate/expand inference tasks. fn is pure value computation over its arguments; mutate is the only side-effecting verb; check emits diagnostics observer-only (lowers to Cat3 — never populates the IDB).
Purity ladder
| Mode | Callable from | Calls | Lowers to |
|---|---|---|---|
fn | any | fn, query | pure compute |
query | query, derive, mutate, check | fn, query | Cat1 + projection |
derive | derive, mutate (head population), check | fn, query | Cat1 (monotone) or Cat2 (NAF) |
check | observers only | fn, query | Cat3 (observer) |
mutate | host, other mutate | all five | side-effecting |
fn and query are cacheable on (args, state_version). derive is stratified fixpoint. mutate is transactional.
fn
fn-decl ::= attribute* 'pub'? 'fn' Ident generic-params? '(' param-list ')' '->' TypeExpr fn-body
fn-body ::= '=' expr ';' // single-expression
| '{' stmt* expr? '}' // block, implicit return
param ::= 'self' | Ident ':' TypeExpr
self and Self. Argon is value-semantic; there is no reference or borrow form. self is always owned-by-value (no &self / &mut self) and is admitted only as the first parameter of a method inside an impl block or a trait declaration; its type is the enclosing impl’s target. Self resolves to the implementing type in trait declarations, to Type inside impl Trait for Type, and to Type inside bare impl Type { … }.
pub fn rent_per_day(l: Lease) -> Money = l.monthly_rent / 30;
pub fn classify(area: Decimal) -> Size {
if area < 50.0 { Size::Small }
else if area < 200.0 { Size::Medium }
else { Size::Large }
}
pub fn safe_div(a: Money, b: Money) -> Result<Money, Diagnostic> {
if b == 0 {
Err(Diagnostic { severity: Severity::Error, code: "Math::DivZero", message: "..." })
} else {
Ok(a / b)
}
}
derive
derive-decl ::= attribute* 'pub'? 'derive' Ident '(' param-list ')'
( ':-' atom-list )? ';'
atom-list ::= atom (',' atom)*
atom ::= predicate-call | comparison | type-test | 'not' atom | path-pattern
pub derive ancestor(d: Person, a: Person) :- ParentOf(d, a);
pub derive ancestor(d: Person, a: Person) :- ParentOf(d, p), ancestor(p, a);
pub derive senior(p: Person) :- p: Person, p.age >= 65;
// Ground-fact form (no body, concrete args): the head holds for the named terms.
pub derive iof(Type, Type); // MLT* OL3
pub derive Within(usc26, usc); // seed a derived predicate
Multiple derives with matching head name and arity compose as Datalog union (modulo the stratification check below). not atom is NAF, evaluated under well-founded semantics. A derive with no :- clause is bodiless, and its head arguments decide its reading:
- Concrete arguments → a ground fact. When every head argument is a concrete term — a declared individual (
Within(usc26, usc)), an enum constant, an axis value, or a type reference (iof(Type, Type)) — the bodilessderivedeclares that tuple as a ground fact, equivalent to:- true. This is the sanctioned way to seed ground tuples directly on a derived predicate: the seed tuples union with the head’s otherderiveclauses and rules over the same relation node (same name, same arity), so a recursive rule reads them and computes their closure (seeds ∪ derived-closure). A non-concrete (free-variable) argument is the error — a bodilessderivehas no body to range-restrict it — and is refused withOE1342, the bodiless analogue ofOE1303(below), naming the offending argument and directing you to declare the individual or add a:- …body. - Type-annotated parameters → an empty predicate. A bodiless head whose arguments are typed parameters (
pub derive adult(p: Person);) introduces the predicate with that signature and an initially empty extent — the introduce-empty idiom (Rule-atom grammar). Its rows arrive from later same-headderiveclauses.
Every body-carrying rule must be range-restricted (safe): each variable in the head, in a negated (NAF) atom, or in a comparison/compute operand is bound by some positive body atom; an unsafe rule is refused at compile (OE1303), since an unbound variable would silently project Null or mis-evaluate.
Evaluation model. A derive program denotes the tuples its rules derive, under the semantics fixed in Reasoning: the least fixpoint of the positive program, stratified negation layered along the axis dependency graph, and — for a negation cycle no stratification can layer — the well-founded model computed by the alternating fixpoint. The surface consequences a rule author relies on:
- Recursion through negation is accepted. A negation cycle (
p :- not q,q :- not p) that strict stratification cannot layer is evaluated under well-founded semantics rather than refused. Cross-stratum, acyclic NAF is ordinary stratified negation. (OE1309no longer fires from stratification; it survives only as the dispatch seam.) - Two-valued query surface. WFS is three-valued (true / false / undefined), but the engine materializes only the definitely-true extent — a paradoxical atom resolves to
undefinedand does not fire. A rule whose conclusion is genuinely undecided neither fires nor is denied. #[brave]/ stable-model semantics is out of scope (Out of scope).
Parametric (axis-generic) rules. A derive rule may be generic over a set of axes; the elaborator monomorphizes it to a finite set of concrete rules at elaboration, with no change to fixpoint semantics.
Stratified aggregates. Aggregate atoms (count, sum, max, min, avg, set_collect, …; see query) are admitted inside derive bodies when the aggregated predicate sits at a strictly lower stratum than the rule head, where the stratification dimension is a well-founded relation (typically the iof DAG, the specialization lattice, or an explicit user-declared ordering). The classic library use case is computing a derived level function over iof:
pub derive has_order(t: Entity, n: Nat) :-
n == 1 + max { select m from t': Entity, m: Nat
where iof(t', t), has_order(t', m) };
The stratifier accepts this because the inner max aggregates has_order over iof-predecessors of t, and iof is well-founded. Stratified aggregates that would loop through an aggregation step on the same stratum are rejected with OE0510 NonStratifiedAggregate.
Universals over recursive predicates. A common composite pattern — a conjunction is fulfilled iff all of its children are — is a universal over the very predicate being defined:
pub derive Fulfilled(p: Conjunction, t: Instant) :-
Instant(t), forall c: PC where childOf(p, c), Fulfilled(c, t);
This form is refused (OE1317 RecursionThroughAggregation): the forall lowers to the count-equality aggregate (Rule-atom grammar), and stratified-aggregate semantics (Faber–Pfeifer–Leone) require the aggregated predicate in a strictly-lower stratum — here Fulfilled aggregates over itself, so its SCC crosses an aggregate boundary and has no stratification. No intermediate derive helps: recursion depth follows the data (the part-whole tree), so any helper joins the same cycle. The supported phrasing is negation-as-failure double negation — ∀c.F(c) rewritten as ¬∃c.¬F(c):
pub derive HasUnfulfilledChild(p: Conjunction, t: Instant) :-
childOf(p, c), Instant(t), not Fulfilled(c, t);
pub derive Fulfilled(p: Conjunction, t: Instant) :-
Conjunction(p), Instant(t), not HasUnfulfilledChild(p, t);
This converts recursion-through-aggregation into recursion-through-negation, which the well-founded executor evaluates. On two-valued-total data — every child’s Fulfilled status resolves to true or false, as it does over a finite acyclic part-whole tree of leaves with definite status — the double negation derives exactly the universal’s extent, bottom-up through the tree. The honest caveat: where a child’s status is genuinely undefined under WFS, the double-negation form propagates undefined to the parent rather than guessing either way — and the two-valued surface omits undefined atoms, per the projection rule under well-founded semantics. Admitting the forall form directly (structural stratification over a provably well-founded relation) is tracked in issue #185.
Worked example — double-entry accounting (RFD 0029). Derived values and aggregate-as-term together make quantitative domains authorable. The double-entry invariant — within every journal entry, total debits equal total credits — is a check comparing two aggregates; the per-account balance is a derived value, grouped per account (the outer bound variable). The running package is examples/double_entry_v0.
pub type Account { mut name: String, }
pub type Entry { mut memo: String, }
pub type Posting { mut amount: Decimal, mut side: String, } // side = "D" | "C"
pub rel postedTo(posting: Posting, account: Account);
pub rel inEntry(posting: Posting, entry: Entry);
// Per-account balance = Σ debits − Σ credits, grouped per `acct`.
pub derive accountBalance(acct, bal) :- acct: Account,
debits = sum(p.amount for p in Posting, postedTo(p, acct), p.side == "D"),
credits = sum(p.amount for p in Posting, postedTo(p, acct), p.side == "C"),
bal = debits - credits;
// The double-entry invariant — two aggregates compared, per entry.
pub check EntryNotBalanced(e: Entry) :- e: Entry,
debits = sum(p.amount for p in Posting, inEntry(p, e), p.side == "D"),
credits = sum(p.amount for p in Posting, inEntry(p, e), p.side == "C"),
debits != credits
=> Diagnostic {
severity: Severity::Error,
code: "Ledger::E001",
message: "journal entry is not balanced — total debits must equal total credits",
};
// A sales-tax line, rounded to cents with banker's rounding (the money default).
pub derive accountTax(acct, tax) :- acct: Account,
debits = sum(p.amount for p in Posting, postedTo(p, acct), p.side == "D"),
tax = round_half_even(debits * 0.075, 2);
Every amount is an exact Decimal: sum folds rationals (no rounding), the balance subtraction is exact, and round_half_even rounds to cents exactly — 150.75 * 0.075 = 11.30625 rounds to 11.31, never an f64 artifact. The EntryNotBalanced check is an instance-level Error, so at runtime it is a delta guard: a mutation that would leave an entry unbalanced is rejected atomically (double-entry is enforced, not merely reported).
Rule-atom grammar
Bodies of derive, query from clauses, check, and unsafe logic blocks are conjunctive sequences of rule atoms. The atom shapes:
rule-body ::= rule-atom (',' rule-atom)*
rule-atom ::=
// Negation as failure
'not' rule-atom
| 'not' '(' rule-atom (',' rule-atom)* ')'
// Modal (tier:modal)
| 'box' '(' rule-atom ')'
| 'diamond' '(' rule-atom ')'
// Restriction quantifiers — DL ∀R.C / ∃R.C (tier:expressive)
| 'forall' '(' field-path ',' type-expr ')'
| 'exists' '(' field-path ',' type-expr ')'
// FOL binding quantifiers — admitted at tier:fol (inside unsafe logic)
| 'forall' Ident ':' type-expr 'where' rule-body
| 'exists' Ident ':' type-expr 'where' rule-body
// Aggregate as atom, optionally compared (paren form, Expression grammar)
| aggregate (comp-op expr)?
// Subquery as atom, optionally compared (brace form, `query`)
| subquery (comp-op expr)?
// Reflection
| meta-call (comp-op expr)?
| path '::' Ident // sugar for meta(path) == Ident
// Predicate call with optional outcome / comparison suffix
| path '(' rule-arg (',' rule-arg)* ')'
('is' outcome-suffix | comp-op expr)?
// Role invocation with closure (+ = transitive, * = reflexive-transitive)
| field-path ('+'|'*') '(' Ident (':' type-expr)? ')'
| field-path '(' Ident (':' type-expr)? ')'
// Type test and `is` sugar
| field-path ':' type-expr
| field-path 'is' ('not'? 'unknown' | type-expr | Ident)
// Specialization
| field-path '<:' field-path
// Binding — single `=`, distinct from the comparison `==` (RFD 0029)
| Ident '=' expr
// Comparison — RHS is a full expression (Expression grammar)
| field-path comp-op expr
// Membership — RHS is a full expression (set, list, or range)
| field-path 'not'? 'in' expr
// Bare-path Boolean test
| field-path
field-path ::= Ident ('.' name)*
rule-arg ::= expr | expr comp-op expr
outcome-suffix ::= 'not'? ('ambiguous'|'unknown'|'timeout') ('(' Ident ')')?
Binding atoms (x = expr, RFD 0029). A rule body may bind a fresh variable to the value of an expression: x = expr (single =) is assignment, distinct from the comparison x == e (double =, a filter). expr ranges over bound variables, literals, field projections (t.income), exact arithmetic over the numeric tower, the rounding builtins (Expression grammar), and aggregate expressions. This is the established Datalog/Soufflé assignment concept and is what lets a rule head carry a derived value:
pub derive Tax(t, owed) :- appliesTo(b, t), owed = t.income * b.rate;
The binding introduces a fresh variable, and is range-restricted. Two distinct refusal paths, never a silent Null:
- Freshness (
OE1335 BindingLhsAlreadyBound). The left-hand sidexmust be a new name. Ifxis already bound — by a prior positive predicate atom, a head parameter bound elsewhere, a projection, an aggregate result, or an earlier binding — the=would silently degrade into an equality filter (xjoined against the computed value) rather than a binding. It is refused, namingx, with the directed hint: use==to compare, or pick a fresh name. This is the path a rebindx = x + 1takes whenxis otherwise bound (e.g. by a body predicate) — the LHS is not fresh. - Range restriction (
OE1303 RuleNotRangeRestricted).x = exprbindsxpositively iff every variable inexpris itself positively bound. An unbound right-hand-side variable, or a pure self-reference / cycle among binding atoms where the result variable is bound by nothing else (x = x + 1as the only binder ofx;x = y, y = x), leavesxunbound under the binding fixpoint and is refused as unsafe.
So x = x + 1 refuses either way — via OE1335 when x is already bound elsewhere (freshness), or via OE1303 when x has no other binder (range restriction) — and the two codes name the two genuinely different errors.
Aggregates as bindable terms (RFD 0029). Because an aggregate is a bindable expression, comparing two aggregates — the double-entry sum(debits) == sum(credits) invariant — is simply binding each and comparing the bound variables:
pub derive balanced(e) :- e: Entry,
debits = sum(p.amount for p in Posting, inEntry(p, e), p.side == "D"),
credits = sum(p.amount for p in Posting, inEntry(p, e), p.side == "C"),
debits == credits;
The aggregate source extends the comprehension form: after for x in Source, a comma-separated list of additional body atoms (relation atoms, comparisons, type tests) refines the fold’s domain, and variables from the outer rule body are visible inside. That visibility is the grouping — the group key is the set of outer bound variables free in the aggregate, the standard Datalog reading. pub derive balance(acct, s) :- acct: Account, s = sum(p.amount for p in Posting, postedTo(p, acct)); groups per acct. A binding is not a comprehension trailing-atom form — sum(w for u in T, …, w = u.v) is refused with OE1336 BindingInComprehension, which directs you to project the value into the fold directly (sum(u.v for u in T, …)) or to bind in the outer rule body and aggregate the bound variable. The brace form (sum { … }) stays count/exists-only; a value aggregate in brace form is refused with OE1331 ValueAggregateBraceForm, which directs to the comprehension form. The SQL-ish group by … having is not built; it refuses with OE0007 carrying the binding-form rewrite. (Aggregates evaluate over the definitely-true extent: an aggregate folding over a relation with well-founded-undefined atoms is refused at runtime with OE1332 AggregateOverUndefined rather than silently treating undefined as false. The refusal is whole-relation, not per-group; three-valued aggregate intervals are tracked under issue #250.)
Empty-group semantics — a deliberate split. When a group is empty (the fold sees no rows), the aggregators divide by role:
sum,count,count_distinctemit a value for the empty group —0. An additive/cardinality fold has a well-defined identity (the empty sum is zero, the empty count is zero), so the grouped row is produced with that identity value.min,max,avgdrop the row — they have no value over an empty set (there is no least/greatest element, and the mean is0/0), so no grouped row is produced for an empty group rather than fabricating one.
This split is load-bearing for the double-entry invariant. sum(debits) == sum(credits) must catch an entry that has credits but no debits: because sum emits 0 for the empty debit side, the comparison is 0 == credits, which fails and flags the entry as unbalanced. Were sum to drop the empty group, the row would vanish and the imbalance would pass silently. The examples/double_entry_v0 package relies on exactly this behavior.
Conjunction between atoms uses ,. NAF uses the not keyword. Disjunction is not inline — write separate derive rules with the same head and arity; the union composes structurally. The same-head idiom is derive-only: a check head carries payload identity (its => Diagnostic { severity, code, message } report, its guard classification, its violation relation), so two checks sharing one head would cross-talk and are refused at elaboration (OE1328 DuplicateCheckHead) — rename one check, or express the disjunction through a shared derive predicate whose same-head clauses union, read by a single check. (Trait check members are unaffected: each impl’s monomorphized member rule is qualified by its impl target and keeps a distinct head, Trait atom.) The RHS of comparison atoms is a full expression (Expression grammar); inside that expression context, && / || / ! apply normally.
Payloadless enum constants. In value position, a path of the form EnumName::VariantName denotes the canonical enum value when the variant has no payload. This is distinct from the standalone rule atom sugar path :: Ident above, which means meta(path) == Ident. Payload-carrying variants require constructor semantics and are not constants by themselves.
Predicate resolution. The head of every predicate-call atom (path '(' … ')') must resolve to a declared predicate in scope — a rel, a concept used as a classification predicate, a derive / query head, a trait rule member (qualified Trait::member(...), or bare when exactly one provider is in lexical scope; subject to the coverage gate, Resolution), a pub fact / pub not_fact predicate, a fn, or a reflection intrinsic (iof, specializes, meta, extent, implements). check heads are not consumable: checks are observers only (Purity ladder) — their violation sets never populate the IDB — so a body atom resolving to a check head (module-level or trait check member) is refused with OE1329 CheckHeadConsumed; derive from the underlying body predicates instead (factor the violation pattern into a pub derive head read by both the check and the consuming rule). This applies to the predicate atoms of derive and check bodies and to both the body and head atoms of bridge rules (Bridge rules). An unresolved head is OE0223 RuleReferencesUnknownPredicate, the rule-body analogue of the pub fact obligation OE0220 (RFD 0004). A resolved atom is further checked for arity (OE0225) and — where both the argument’s type and the declared parameter type are concretely known — argument type (OE0226, sound under multiple classification: it fires only on provable disjointness). This is independent of the world assumption: the world assumption governs the truth value of instances of a declared predicate (under the CWA default an unasserted instance is false; under an #[world(open)] concept it is unknown/Can, World assumptions (CWA / OWA)) — it never admits an undeclared predicate name. To introduce a predicate that is intentionally empty until populated, declare it (pub rel P(...), or a bodiless pub derive P(...); head). ox check enforces this, and ox build refuses to emit an artifact for a program containing an unresolved predicate.
pub fact targets a base predicate, never a derived one. A derive / query head is a valid rule-body atom (it is in the resolution set above), but it is not a valid pub fact target. pub fact asserts into an extensional relation (a concept-as-classification or a pub rel); a derived predicate’s extent is intensional — computed by rule derivation. A pub fact P(...) whose P is a pub derive / pub query head is refused with OE0239 FactReferencesDerivedPredicate: the asserted seed tuple would key a different relation node than the rule head reads, so it would silently drop out of the rule’s fixpoint — the derive would evaluate to a result that omits the asserted tuples (no error, wrong answer). To give a derived predicate ground tuples that participate in its derivation, use one of two sanctioned forms, and OE0239’s help names both:
- A bodiless
pub derive P(args);clause over concrete arguments (derive). The seed tuple is itself aderiveclause onP, so it sharesP’s relation node and participates directly inP’s fixpoint — the simplest way to seedPitself. - A base relation the rule reads — declare
pub rel Base(...), seed it withpub fact Base(...), and add a clausepub derive P(...) :- Base(...). Use this when the seed tuples are themselves a reusable extensional predicate.
Either keeps P a single-origin intensional head (the same rule Build pipeline and .oxbin enforces for a foreign-placed-vs-derived relation, OE1246). (A pub fact over a check head is the ordinary OE0220 — a check head is observer-only, not a predicate at all.)
Build loud-gate. More broadly, ox build refuses (writing no .oxbin) any derive / query rule the engine does not evaluate — an unsupported quantifier shape, a nested or non-count-class aggregate, not <aggregate>, an unsupported type-test or meta-eq (OE1311–OE1315). Argon refuses rather than emit an artifact that would silently mis-derive; see Build pipeline and .oxbin.
Modal atoms (box(...), diamond(...)) carry Kripke-frame semantics over the standpoint and classification frames described in Modal operators. The elaborator statically discharges the common case (type-classification atoms whose target is introduced by a fixed metatype — membership constant, RFD 0027 D6) and routes the remainder to a modal reasoner over std::kripke. Modal atoms are admitted only at tier:modal; lower tiers reject them.
Restricted universals (forall v: T where Body, Head). In the where body the last atom is the consequent (Head) and all preceding atoms are the domain restriction (Body). The quantifier holds iff every domain element also satisfies the consequent — it lowers to the count-equality count{ v : Body, Head } == count{ v : Body } (the domain-and-consequent count equals the domain count). The type annotation T is the static sort of v; the runtime domain comes from the where Body, so the Body must restrict v with a membership or predicate atom (e.g. member(g, v) or v in g.items) — Body, not T, bounds the count. An empty domain (count{ v : Body } == 0) makes the universal vacuously true (0 == 0), deriving the head — classical restricted-∀ semantics. The encoding needs at least a domain atom and a consequent (≥2 where atoms); the paren restriction form forall(path, T), the exists-binder form, and a single-atom where refuse with OE1315 (App. C).
Allen interval relations (before, meets, overlaps, during, starts, finishes, and their reciprocals) are not substrate operators. They are provided by the std::allen library (RFD 0024), defined over the Date / Duration value layer (Temporal substrate) — ordinary predicates over interval endpoints, not parser-level syntax.
Three atom shapes in the grammar above parse-refuse rather than silently mis-derive:
- Role closure
field-path ('+'|'*') '(' … ')'(e.g.p.knows+(q: Person)). The base role-invocation formfield-path '(' Ident (':' type-expr)? ')'is accepted; the transitive (+) / reflexive-transitive (*) closure suffix refuses withOE0001. Express transitive reachability through a recursivederivehead. - Axis sugar
path '::' Ident(x :: T, i.e.meta(x) == T). Write themeta(x) == Tcomparison atom directly; the::rule-atom shorthand refuses. (In value positionEnumName::Variantis unaffected — see the payloadless-enum-constant note above.) - Range membership
field-path 'not'? 'in' <range>(p.v in 1..10). The set/list RHS ofinis accepted; alo..hirange RHS refuses (OE0001on..). Write the two-sided comparisonp.v >= 1, p.v <= 10instead.
Temporal rule atoms
Argon’s bitemporal substrate (Temporal substrate) admits metric temporal operators in rule bodies — the DatalogMTL fragment with stratified negation over the integer timeline.
Any atom may be qualified by a valid-time point or interval: atom at t (a single VT point), atom during [t1, t2] (a closed VT interval), atom since t (the open interval [t, ∞]).
Six prefix metric operators address past and future. Their interval bounds are durations (0, N with a unit ns…y, or inf/∞):
box_minus [a, b] (φ) // φ held at every past point in [a, b]
diamond_minus [a, b] (φ) // φ held at some past point in [a, b]
box_plus [a, b] (φ) // φ holds at every future point in [a, b]
diamond_plus [a, b] (φ) // φ holds at some future point in [a, b]
since [a, b] (φ, ψ) // φ has held since ψ within [a, b]
until [a, b] (φ, ψ) // φ holds until ψ within [a, b]
Two shortcuts expand to [0, ∞] intervals: ever atom (some past or future point; needs tier: expressive for the disjunction) and always atom (every point; tier: recursive).
Side-by-side modal (box/diamond) and metric temporal atoms in one body compose additively. Nesting one family inside the other is refused at tier: recursive and routed to tier: fol — see Tier ladder for the decidability rule (OE0712).
query
query-decl ::= attribute* 'pub'? 'query' Ident '(' param-list ')' '->' TypeExpr
('across' '[' standpoint (',' …)* ']')?
'{' query-body '}'
query-body ::= ('with' Ident '=' query-body ';')*
'select' projection
'from' rule-body // Rule-atom grammar
('optional' 'from' rule-body)*
('where' rule-body)? // additional atoms
('group' 'by' expr ('having' expr)?)?
('order' 'by' expr ('asc' | 'desc')?)?
('limit' Nat ('offset' Nat)?)?
projection ::= expr | '{' struct-init-list '}' | 'path' Ident | aggregate
role-step ::= Ident quantifier? ('(' role-binders ')')? ('as' Ident)? mode-spec?
quantifier ::= '+' | '*' | '{' expr (',' expr)? '}' // expr of type Nat
mode-spec ::= 'mode' ('walk' | 'trail' | 'acyclic' | 'simple')
| 'shortest' | 'all-shortest' | 'any' | 'k-shortest' Nat
Role-step path-traversal chains (a.role+(b), c.ParentOf{1, n}(p: Person)) are admitted as predicate-call atoms in the rule-atom grammar (Rule-atom grammar) and are not separately defined here.
The evaluated query-body is select … from … (where …)?. Every other form
parse-refuses loudly with a feature-named code — never a generic
OE0001, never silently ignored: the result-shaping clauses (group by … having, order by … asc|desc, limit … offset) with OE0007; the
with CTE clause with OE1344; the graph-/path-traversal projections
(select shortest path …, select path …, … as <name> mode …) with
OE1345; optional from with OE1346; and the result-table set
operators (union / union all / intersect / except) with OE1347.
pub query active_leases_for(t: Person) -> [Lease] {
select l from l: Lease, l.tenant == t where is_active(l)
}
pub query ancestors_within(p: Person, n: Nat) -> Set<Person> {
select a from p.ParentOf{1, n}(a: Person)
}
pub query shortest_route(a: City, b: City) -> Path<City, Road> {
select shortest path r from a.road+(b) as r
}
pub query avg_age_by_dept() -> Map<Department, Real> {
select { dept: avg(p.age) }
from p: Person, p.works_in(dept: Department)
group by dept
}
pub query rich_friends_of(p: Person) -> Set<Person> {
with rich = select x from x: Person where x.net_worth > 1_000_000;
select f from p.knows(f: Person) where f in rich
}
pub query agreed_across(p: Person) -> Truth4Of<Bool>
across [LegalGround, MedicalGround]
{
select consents(p)
}
Subquery forms. Brace-bracketed inner queries used as expressions:
subquery ::= subquery-kind '{' rule-body '}' // Rule-atom grammar; no `from` keyword
subquery-kind ::= 'collect' | 'set' | 'one' | 'count' | 'exists'
The subquery body is a bare rule-atom list — there is no from keyword inside the braces (that prefix belongs to the top-level query-body only; an inner from parse-refuses with OE0001).
| Form | Returns | Notes |
|---|---|---|
collect { … } | List<T> | Projection-carrying; supports order/limit. |
set { … } | Set<T> | Projection-carrying; deduplicated. |
one { … } | Option<T> | Projection-carrying; None if no match. |
count { … } | Nat | No projection (counts matched tuples). |
exists { … } | Bool | No projection (K3 fail-closed; false on unknown). |
The cardinality-fold kinds count and exists evaluate over the bare atom
list above (count { p: Person }, exists { p: Person }). The
projection-carrying kinds collect / set / one parse but refuse at
ox check / ox build (OE1312); they never silently lower to a
cardinality fold. Use a derive head plus a top-level query projection.
Set operations on result tables: union, union all, intersect, except.
Subquery aggregates (admitted as aggregate projections inside subquery bodies and as the subquery-form <aggregate> { atoms }): count, count distinct, sum, avg, min, max, collect, set_collect, string_join, percentile. The expression-level comprehension form sum(expr for x in coll where pred) (Expression grammar) admits a subset (sum, count, min, max, avg) for non-subquery contexts.
Default lattice context: K3. With across [...]: FDE.
mutate
mutate-decl ::= attribute* 'pub'? 'mutate' Ident '(' param-list ')' ('->' TypeExpr)?
'{' mutate-body '}'
mutate-body ::= ('require' '{' expr (',' …)* '}')?
stmt*
('return' expr ';')?
stmt ::= 'let' Ident (':' TypeExpr)? '=' expr ';'
| 'match' expr '{' arm (',' …)* '}'
| insert-stmt | update-stmt | delete-stmt | upsert-stmt
| 'detach' 'delete' expr ';' // ‡ refused, OE1353
| emit-stmt
| 'for' Ident 'in' expr '{' stmt* '}'
| 'if' expr '{' stmt* '}' ('else' '{' stmt* '}')?
| expr ';'
insert-stmt ::= 'insert' TypeExpr '{' field-init-list '}' ';' // typed-literal insert
| 'insert' Ident ':' TypeExpr '{' field-init-list '}' ';' // ‡ named typed-literal — OE0001
| 'insert' Ident 'into' expr ';' // insert binding into collection
| 'insert' predicate-call '{' field-init-list '}' ';' // relation insert with body
| 'insert' predicate-call ';' // relation insert, no body
update-stmt ::= 'update' (Ident | pattern) 'set' '{' field-assign (',' …)* '}' ('where' expr)? ';'
field-assign ::= Ident ('=' | '+=' | '-=') expr
delete-stmt ::= 'delete' predicate-call ';' // delete iof(…) / delete Rel(…)
| 'delete' (Ident | pattern) ('where' expr)? ';' // ‡ entity / bulk delete — OE0001
upsert-stmt ::= 'upsert' pattern ('as' Ident)? upsert-clause+ ';' // ‡ refused, OE1352
upsert-clause ::= 'on' 'insert' '{' field-assign (',' …)* '}'
| 'on' 'update' '{' field-assign (',' …)* '}'
emit-stmt ::= 'emit' Ident '{' expr '}' ';'
pub mutate sign_lease(t: Person, p: Property, rent: Money, term_days: Nat) -> Lease {
require { rent > 0, term_days > 0 }
let l = insert Lease(t, p) {
monthly_rent: rent,
start: today(),
end: today() + term_days.days,
status: Pending,
};
emit AuditLog { LeaseSigned { lease: l, at: now() } };
return l;
}
pub mutate hire_or_raise(p: Person, o: Organization, salary: Money) {
upsert p.works_at(o) as emp
on insert { emp.start_date = now(), emp.salary = salary }
on update { emp.salary = salary };
}
pub mutate rebrand(old: String, new: String) {
update c: Company set { name = new } where c.name == old;
}
An update of a bound target writes its mut fields directly. The
('where' expr)? filter (and the bulk/pattern target forms it implies, as
in rebrand above) is refused at ox check / ox build (OE1318), never
silently dropped.
The grammar shapes marked ‡ are loud refusals, not silent no-ops. Named
typed-literal insert (insert l: Lease { … }; use let l = insert Lease { … } instead) and entity/bulk delete of a bound target or pattern with an
optional where refuse with OE0001 — only delete predicate-call (delete iof(…) / delete Rel(…)) is admitted. detach delete refuses with OE1353
and upsert with OE1352.
The rebrand example assumes name is declared mut on Company. Per struct and enum — language built-ins (data declarations), fields are immutable post-construction unless explicitly marked mut:
pub type Company {
#[intrinsic] founded: Date, // construction-required; not updatable
mut name: String, // updatable via `update`-stmt
}
update-stmt admits writes only to mut fields. A field-assign targeting a non-mut field is rejected with OE0820 UpdateImmutableField. The elaborator validates each field-assign against the entity’s field-decl mut flag; rejection surfaces at build time, not at mutation invocation.
Field updates lower to append-only event pairs on the underlying property axiom (Storage layer): a retract event for the prior value and an assert event for the new. The proposition’s logical identity is the (entity_id, property_id) pair; the value is what changes. Bitemporal queries reconstruct prior values per Temporal substrate.
Mutations are transactional within the body; the kernel may reject mutations that violate refinement, consistency policy, or stratification — rejection surfaces to the caller as Result<T, Diagnostic> from the host runtime.
Dynamic reclassification — insert iof / delete iof. Because iof is a first-class predicate (Reflection), mutate bodies can dynamically classify and declassify entities by inserting or deleting iof tuples through the existing insert predicate-call / delete predicate-call forms:
pub mutate enrol(p: Person) {
insert iof(p, Student); // p is now a Student
}
pub mutate expel(s: Student) {
delete iof(s, Student); // s is no longer a Student
}
The single-type insert iof(x, T) / delete iof(x, T) forms above classify and declassify an entity, subject to the gates below. Inserting an iof tuple over a relation concept — insert iof((p, into), EnrolledAt) for a named relation EnrolledAt — refuses: the tuple-argument form parse-refuses with OE0001 rather than silently doing nothing.
The substrate enforces two constraints on insert iof(x, T):
- Refinement gate (enforced). If
Tcarries a defined refinementiff { … }(Refinement) — for example,pub type ExtinctSpecies <: BirdSpecies iff { self.numberOfLivingInstances == 0 }— the predicate is the substrate’s source of truth for membership: the modeler updates the underlying state (here,numberOfLivingInstances) and the substrate derives the iof classification automatically, so explicitinsert iof(x, ExtinctSpecies)emitsOE0211 IofInsertOnDefinedand the mutation is rejected. If insteadTcarries a primitive refinementwhere { … }, membership is conferred by assertion, soinsert iof(x, T)is permitted — but the predicate is a necessary invariant: anxthat positively violates it (definitefalse, World assumptions (CWA / OWA)) is rejected withOE0668 RefinementInvariantViolated. The same invariant is checked when a primitive-wheremember is created by construction (insert T { … }) or when anupdatewrites a field the predicate reads. - Modifier gates (enforced — RFD 0027 D6). Re-classification is governed by the ontology-neutral
fixedmodifier on the target’s introducing metatype (metatype), never by any axis name:insert iof(x, T)/delete iof(x, T)whereTis fixed-introduced is refused withOE0234 FixedReclassification— classification under such types is decided at construction (insert <T> { … }remains legal; construction is not re-classification). Admission is the absence offixed: dynamic classification is the default, and a vocabulary opts its rigid metatypes into the restriction explicitly (pub fixed metatype kind = { … };) — an axis assignment likerigidity::anti_rigidis inert user vocabulary and grants nothing by itself. Likewiseinsert iof(x, T)against anabstracttype — a direct instance — is refused withOE0233 AbstractTypeConstruct; non-abstract subtypes are unaffected. Both gates fire atox check/ox buildwhere the target is statically known and at the runtime write path otherwise, rejecting the whole mutation atomically. The membership-constancy theorem (Argon.Runtime.ModifierGates.runMutation_fixed_iof_constant) is what grounds static modal discharge over fixed-introduced types (Modal operators). - Compatibility gate.
x’s existing classification chain must include some supertype thatTspecializes (e.g.Student <: Personrequiresx: Person).
After insert iof(x, T), the entity x is classified under both its prior types and T. After delete iof(x, T), x is no longer classified under T but retains all other classifications. The substrate maintains a single global iof relation; insert/delete operate transactionally.
Bitemporal mutation forms. Per the temporal substrate (Temporal substrate, Effective-dating: valid time and the two as_of axes), an insert may carry an explicit valid-time at <date> qualifier — the assertion’s valid time begins on that civil day (RP-004 mutate). This form executes: the write threads the date onto the event’s bitemporal extent, and an as_of <#date#> query reads it back.
pub type T;
pub mutate enact(x: T, effective: Date) {
insert iof(x, T); // [VT: now → ∞] [TT: now → ∞] — atemporal default
insert iof(x, T) at effective; // [VT: effective → ∞] — effective-dated
}
The window form during [t1, t2], the since open-interval form, and a valid-time-qualified delete (bitemporal retraction — closing a valid-time interval rather than opening one) refuse loudly (OE1330) rather than silently applying at all valid times:
pub type T;
pub mutate retro(x: T) {
insert iof(x, T) during #2020-09-01#; // window VT
}
Explicit erasure — forget. For compliance scenarios (GDPR right-to-erasure, sensitive-data redaction), forget removes the underlying tuple including its bitemporal history:
forget x; // bound individual; capability-gated
Only the bound-individual form forget <expr> is admitted; the forget iof(x, T) and bulk forget … where … shapes refuse with OE0001.
forget is capability-gated at the source level: a mutate body containing forget refuses to build (OE0730 ForgetWithoutCapability) unless the enclosing mutate declaration grants #[allow_forget]. Cascading derived facts are revised via DRed overdelete.
Reasserting a tuple after retraction produces distinct VT intervals on the underlying store — the substrate does not coalesce on adjacent boundaries. Historical retract/reassert is queryable via audit projections.
Body semantics
A mutate body lowers to the Operation IR and runs with all-or-nothing atomic commit: a failed require guard, or any error, emits nothing (RFD 0015). require preconditions, let bindings, typed-literal entity construction with system-minted identity, projection navigation (a.b.c) over committed state, collection inserts (insert … into …) with for iteration, sum / count aggregate guards over comprehensions, index projection (coll[i]), and effectful if/else (control flow is expression-valued; branches are blocks) all execute, returning the body’s tail value.
match in bodies. A value-position match (a let RHS, an update … set value, a tail / return value, or a require guard) matches over constant patterns (payloadless enum constants, literals, or-patterns, _; Pattern matching) and desugars to the same IfExpr chain a value if uses, with exhaustiveness enforced at ox check (OE0203). A statement-position match (arms running effects — the BPMN-gateway dispatch match d { Disposition::Clean => { update … }, Disposition::BreaksFound => { insert … }, _ => {} }) lowers to a right-nested Operation::If chain over the arm blocks’ operations (RFD 0015 Amendment 2): the same ordered first-match semantics, constant-pattern subset, and OE0203 exhaustiveness as the value desugar; arm blocks and if branches admit the full mutate statement set at any nesting depth. upsert, emit, and detach delete in a body refuse by name rather than dying generic or doing nothing silently: emit with OE1318, upsert with OE1352, and detach delete with OE1353.
Mutate-body arithmetic is exact (RFD 0016 / RFD 0029): require guards and let / field expressions route through one canonical evaluation core — pure-Int operands stay checked integers (overflow is a loud error, never a wrap), and any Real / Decimal / Money operand promotes to an exact rational (/ is the field operation, no truncation), so 0.1 + 0.2 == 0.3 holds exactly in a mutate body where it would fail in f64.
Reads see committed state. A body’s projections (
a.b.c) read the store as committed before the mutation; a body does not observe its own not-yet-committed constructions. One consequence: constructing a collection-valued field and theninsert … intothat same field in a single body does not compose (the append seeds from committed state).
check
check-decl ::= attribute* 'pub'? 'check' Ident '(' param-list ')' ':-' atom-list '=>' diagnostic-expr ';'
pub check no_overlapping_leases(t: Person, p: Property) :-
count { from l: Lease where l.tenant == t, l.property == p, is_active(l) } > 1
=> Diagnostic {
severity: Severity::Error,
code: "Lease::E001",
message: format!("Tenant {} has overlapping active leases on {}", t.name, p.address),
};
A check is a denial rule over the well-founded model: the body is the violation pattern,
the => Diagnostic payload is the per-violation report. Checks never contribute facts (Cat3 —
observers, not derivers). check is the only producer for the diagnostic stream from user code.
Discharge — the body’s vocabulary decides, not the author
You never declare a check “compile-time” or “runtime.” Discharge is staged by what the body reads (RFD 0025), the same staging discipline as refinement predicates (Refinement) and modal static discharge (Modal operators):
| Body vocabulary | Discharges at |
|---|---|
Catalog-level — every head parameter and body variable is reflective-sorted (TypeRef, TraitRef, Metatype — RFD 0025 D1 as amended by RFD 0026 D6); atoms read declaration structure (specializes, implements, the type column of iof/meta) | ox check and ox build (editor surfacing via the LSP is tracked follow-on work). The declared catalog is closed at build, so evaluation there is total and final. |
| Instance-level — any variable ranges over individuals | The runtime, at every mutation boundary and on demand — and also at build, over whatever EDB the package itself declares (pub facts, seeded individuals). |
Catalog-level checks are the user-extensible compiler-diagnostic mechanism: a theory package
ships its modeling constraints (OntoClean-style rigidity lints, MLT well-formedness, trait
conformance) as check rules and they surface at ox check / ox build like any compiler
diagnostic, under the package’s own codes (editor surfacing via the LSP is tracked follow-on
work).
The #[static] convention. Compile-time-intent checks carry #[static] by convention.
The attribute does not change where discharge happens (the vocabulary does); it asserts the
intent — if the body later drifts to instance vocabulary, the build fails with OE1322 instead
of silently reclassifying the check to runtime discharge.
A firing Severity::Error check at build is a build failure: ox build writes no artifact,
same discipline as every other loud gate. Warning/Info render and pass.
Runtime semantics — guard on the delta
At each mutation boundary the runtime evaluates the instance-level checks on the committed
state (pre) and on the transaction’s overlay (post = committed + the body’s buffered
events, mutate):
Severity::Errorguards: if the mutation creates violations —violations(post) ∖ violations(pre)is non-empty — the whole body is rejected atomically (nothing flushes,mutate’s atomicity) and the rendered diagnostics return to the caller. This generalizes the built-in write gates (OE0668refinement invariants,OE0211defined-concept inserts) to user-authored constraints.- Pre-existing violations never block. A violation that predates the mutation (a stricter artifact over old data) is reported through the observe channel but does not reject writes — the guard is over the delta, not the absolute state.
Warning/Infoobserve: collected and surfaced — on mutation results, in dispatch responses (thediagnosticssection), and on demand.#[observe]on anErrorcheck opts out of guarding (report-only, legitimate during migration).
The mechanized contract is spec/lean/Argon/Reasoning/Checks.lean: static_discharge_sound
(a catalog-closed body evaluates identically at build and against any store extending the
catalog) and guard_iff (the guard passes iff violations(post) ⊆ violations(pre)).
The Diagnostic payload
Diagnostic and Severity are check-surface syntax interpreted by the compiler (like the
#[default] defeat-plane directive) — no import declares them. Exactly three fields, validated at
ox check (OE1323); code: and message: are always required, and severity: is required
unless the check implements a trait member whose signature pins it
(check Member(Self) => Severity::…; — Trait atom, RFD 0026 amendment, issue
#230), in which case omitting severity: inherits the pinned one (a divergent restatement is
OE0676):
severity:—Severity::Error|Severity::Warning|Severity::Info(closed set).code:— a string literal, namespaced:::-qualified ("Lease::E001","OntoClean::C1"), with the compiler’sOE/OWprefixes reserved (OE1324).message:— a string literal, orformat!("…{}…", args)with positional{}placeholders only; each argument is evaluated per violation tuple and must resolve against the body’s bindings (OE1325).{{/}}escape literal braces.
at: is reserved for span attribution and refused.
Three-valued honesty
Check bodies evaluate over the well-founded model in K3 and fire on definite (is)
violations only — undefined does not fire. Precisely: a violation requires the body to be
well-founded true, so a negated body atom not p(x) contributes only when p(x) is
well-founded false (definitely absent), not merely not-true — an undefined p(x)
arising from recursion-through-negation does not satisfy the negation. A body that would have
fired only because a NAF subject was undefined is surfaced on the observe channel marked
[undefined], never as a violation and never to the delta guard, regardless of the check’s
declared severity. Negation in a check body is negation-as-failure over the well-founded model (the CWA
default, World assumptions (CWA / OWA)): not statute(c.cite) means “no
derivably known statute,” not a claim about reality. Surfacing can-grade (possible)
violations is out of scope.
Delivery contract: a fired check is semantically an emission to the reserved typed sink
Diagnostics: Diagnostic (Sinks). Delivery is direct — the
build diagnostic stream for static discharge, mutation results and the dispatch-response
diagnostics section for runtime discharge.
emit from any rule mode
emit Name { value } publishes value to sink Name. Allowed in any rule body whose required sinks are in scope. In fn and query, emit fires once per fresh evaluation; cache hits do not re-emit. Sinks are observation channels and never affect the queryable extent.
Sinks
A sink is the typed publication channel emit targets:
sink-decl ::= attribute* 'pub'? 'sink' Ident ':' TypeExpr ';'
pub sink AuditLog: LeaseEvent;
pub sink HitlQueue: ReviewTask;
pub sink Notify: Notification;
Delivery (log file, webhook, message queue, HITL ticket) is a runtime concern; the language guarantees only that emitted values match the sink’s declared type. An emit statement in a mutate body refuses with OE1318 (mutate) rather than silently dropping the emission.
Pattern matching
match-expr ::= 'match' expr '{' arm (',' …)* ','? '}'
arm ::= pattern ('if' expr)? '=>' expr
pattern ::= '_'
| literal
| Ident // binder
| TypeExpr '(' pattern (',' …)* ')' // variant
| TypeExpr '{' field-pattern-list '}' // record
| Ident ':' TypeExpr // type test
| 'is' reasoning-outcome
| pattern '|' pattern // OR
reasoning-outcome ::= 'unknown'
| 'ambiguous' '(' Ident ')'
| 'timeout' '(' Ident ')'
| 'both' '(' Ident ',' Ident ')'
pub fn classify(l: Lease) -> Status =
match l {
ResidentialLease(r) if r.bedrooms > 4 => Status::Large,
ResidentialLease(_) => Status::Standard,
CommercialLease(_) => Status::Commercial,
_: TerminatedLease => Status::Closed,
is unknown => Status::Pending,
is ambiguous(x) => Status::Ambiguous(x),
_ => Status::Unknown,
};
Exhaustiveness checked; non-exhaustive matches require _ or fail with OE0203.
Match semantics
match evaluates — in value position and statement position — as ordered
first-match over constant patterns:
| Pattern form | Admitted | Notes |
|---|---|---|
wildcard _ | ✓ | Matches everything. |
literal (Int / String / Bool / Date) | ✓ | Value equality (Int widens per Engine / Module / Store factoring). |
payloadless enum constant path (Status::Active) | ✓ | Resolved through the enum resolver; canonical-CBOR equality at runtime. |
or-pattern of the above (A | B => v) | ✓ | Expands to consecutive arms sharing the body. |
binder Ident | refused OE1319 | |
variant payload Type(p) (one positional binder) | ✓ value position | Binds the payload over the arm body; refused OE1319 in a statement-position effectful arm (below). A zero- or multi-binder arm is OE0257; the record form Type { … } is refused. |
type test x: T | refused OE1319 | |
is reasoning-outcome | refused OE1319 | |
arm guard pat if cond | refused OE1319 |
A value-position match (a fn body, a rule-body comparison operand, a
mutate let RHS / update … set value / return value / require guard)
desugars at elaboration to a right-nested conditional chain over
scrutinee == constant tests — if s == P1 { v1 } else if s == P2 { v2 } else { v_last } — so it executes on the existing IfExpr paths in the
reasoner and the mutate runtime; semantics are first-match in arm order,
and the final arm’s value is the else-branch. Exhaustiveness is
required: a final _ arm, or full coverage of the scrutinee’s enum
variants when statically known — otherwise OE0203 at ox check.
Statement-position match (arms running effects inside a mutate
body) executes with the same semantics (RFD 0015
Amendment 2): the arms are blocks
running mutate statements (update / insert / delete / forget /
require / nested for / if / match), the match lowers to a
right-nested Operation::If chain over the arm blocks’ operations —
ordered first-match, exactly one selected arm’s effects run, the arm
value is discarded — and the same exhaustiveness rule applies (_ => {}
with an empty block is the idiomatic catch-all; OE0203 otherwise).
Or-patterns expand to consecutive conditions sharing the arm’s
operations.
A single-binder payload destructuring (Some(v) => …) executes in
value position — let r = match opt { Some(v) => v, None => 0 }; and
every value-position site above: the arm’s condition tests the variant
tag and its body sees the binder substituted by the decoded payload. A
payload-binding arm in statement position (an effectful arm inside a
mutate statement-match) refuses with OE1319: bind the payload in
value position first (let v = match opt { … };), then run the effect. A
bare binder Ident, a type test, an is-outcome arm, and an arm guard
refuse with OE1319 in both positions.
Rule-body arm projections must be total. In a rule body (derive /
check / query / bridge), the relational lowering hoists every field
projection inside the conditional chain — including arms the scrutinee
never selects — into an eager, unconditional $field:: join, because
the relational plan has no lazy join. $field::<f> has no row when an
individual lacks f, so an arm projecting an optional (T?) field
would silently filter out every row missing that field, even rows whose
matching arm never touches it. Rather than mis-evaluate, ox check /
ox build refuse such a rule with OE1320: every field projected inside
a conditional arm (a value-if branch or match arm — both lower
identically) must be total, i.e. declared required on every visible
type that declares it. Workarounds: split the conditional into one rule
per arm (each joining only the fields that arm needs), or declare the
field required. Projections in always-evaluated positions (the rule body
proper, the outermost condition / scrutinee test) and fn / mutate
bodies (which evaluate branches lazily) are unaffected.
Trait atom
A trait is a behavioral contract: a named bundle of obligations over a Self type, discharged
by impl Trait for Type blocks. The boundary with the concept lattice is doctrinal: concepts say
what something is (ontologically); traits say what something can do — trait hierarchies never
enter the <: concept lattice, traits cannot be specialized or inherited, and classification never
belongs in a trait (Modeling guidance: traits versus phases and categories). The full design is RFD 0026;
the trait surface lands in stages, and what runs end-to-end is exercised by the packages in
examples/.
Surface
trait-decl ::= attribute* 'pub'? 'trait' Ident generic-params?
(':' TypeExpr ('+' TypeExpr)*)? -- supertraits (requires-constraints)
('{' trait-item* '}')? | ';' -- empty body = marker trait
trait-item ::= fn-signature ';' -- invocation-plane member ('self' leads)
| 'mutate' Ident '(' 'self' (',' member-params)? ')' ';' -- invocation-plane member
| 'derive' Ident '(' member-params ')' ';' -- rule-plane member
| 'check' Ident '(' member-params ')'
('=>' 'Severity' '::' Ident)? ';' -- rule-plane member; optional
-- severity pin (issue #230)
| 'query' Ident '(' member-params ')' '->' TypeExpr ';' -- rule-plane member
-- 'type' Ident ';' / 'const' Ident ':' TypeExpr ';' remain reserved
impl-block ::= 'impl' generic-params? TypeExpr ('for' TypeExpr)? ('{' … '}')? | ';'
trait-impl-item ::= fn-decl | mutate-decl -- full bodies
| derive-decl | check-decl | query-decl -- full bodies (rule body; select
-- body for query), Self admitted
When 'for' TypeExpr is present the construct is a trait impl (impl Renewable for Lease)
and its items are trait-impl-items; when absent it is a bare impl (impl Lease { … }),
whose items parse per impl Type { … } — grouping but are gated at
elaboration (OE1326) until inherent members execute.
Conformance (Conformance) governs trait impls only. Impls of either form are standpoint-global:
declaring one inside a standpoint { … } block is refused at elaboration (per-standpoint impls
are an open design question RFD 0026 deliberately does not decide).
Selfresolves only inside trait and impl bodies: in a trait item it is the obligation’s type parameter; in an impl item it denotes the impl’s target type. Misuse is OE0675. A trait member signature must mentionSelfin at least one parameter position (a member that is not about the implementing type belongs at module level — OE0675 says so). MultipleSelfpositions are legal.- Supertraits use
:(pub trait Repaintable: Drawable), Rust’s spelling, because they are Rust’s semantics — requires-constraints, not subsumption (Resolution).<:is reserved for the concept lattice. - Generic parameters remain unbounded-only (Generic parameters); generic members, bounded generics, and trait-side default bodies refuse loudly, never dropped.
Member semantics: two planes
Members split by how they are consumed; both planes elaborate by monomorphization — each impl
member lowers to an ordinary rule (Self ↦ target type) flowing through the standard pipeline
(classifier, stratifier, evaluability gate, reasoner) with no new evaluation machinery.
Rule plane (derive, check, query) — clause union
The trait declares the predicate; each impl contributes a type-guarded clause:
pub trait Adulthood {
derive Adult(Self)
}
impl Adulthood for USPerson { derive Adult(p: Self) :- p.age >= 18 }
impl Adulthood for GermanPerson { derive Adult(p: Self) :- p.age >= 18, p.has_residence }
elaborates to two ordinary rules with one head, Adulthood::Adult — Datalog unions same-head
rules natively, and the head-parameter guards close over <: (World assumptions (CWA / OWA)). Dispatch is derivation:
an atom Adult(p) in
pub derive CanVote(p: Person) :- Adult(p), p.registered
holds per-individual under whichever impl covers the individual’s actual type — the relational
transposition of dyn dispatch, with the no-overlap rule (Conformance) keeping the selection
single-valued per declared type (per-individual multiple classification is defined in Conformance). A
query member carries the full query signature — Self in parameter positions, a Self-free
return type (return-position Self is OE0675: it would need union types or bounded generics)
— and each impl provides a full select … from … body; the trait signature fixes the
projection shape. Query members are declared, conformance-checked, and lowered per impl, and
they surface through the same per-impl query dispatch top-level queries use — each impl’s clause
is queryable under its per-impl identity Trait::member@Target. They are not rule-body
atoms: a query-member atom in a derive/check body is refused loudly at ox check, the same
envelope discipline fn members get (Resolution) — top-level queries are not rule-body predicates
either, and the member form inherits exactly that. A single dispatch endpoint returning the
union of per-impl results is reserved. A check
member monomorphizes to ordinary RuleMode::Check rules; discharge, payload, and
delivery follow RFD 0025 unchanged. A check member’s
signature may pin its severity with the payload-arrow suffix
(check OverLimit(Self) => Severity::Error; — RFD 0026 amendment 2026-06-11, issue #230):
the pin makes blocking behavior part of the trait contract. Under a pin, an impl’s
Diagnostic { … } payload may omit severity: (it inherits the pinned one), may restate
the same severity (harmless), and is refused with OE0676 when it states a different one —
a contract whose blocking behavior varies by implementor is a weak contract. Only the severity
is pinnable: code: and message: stay per-impl, and a full Diagnostic { … } payload on the
trait-side signature is refused (OE1323). An unpinned member keeps per-impl severity freedom.
#[observe] on an impl member whose pinned severity is Error remains legal (a discharge-mode
opt-out, not a severity change).
Invocation plane (fn, mutate) — receiver resolution
A body is a single value or a single transaction; resolution is to exactly one impl. An
invocation-plane member leads with the self receiver — its first parameter must be self
(OE0675 otherwise; further Self-typed parameters are admitted after it) — because the receiver
is what dispatch selects on. A mutate member —
pub trait Closable {
mutate close(self, reason: String);
}
impl Closable for SavingsAccount {
mutate close(self, reason: String) {
update self: SavingsAccount set { status = "closed" }
}
}
— monomorphizes per impl (full RFD 0015 imperative bodies; self is the receiver variable inside
the body, Self splices to the target type) into the mutation catalog under the per-impl identity
Closable::close@SavingsAccount, callable as Closable::close: invocation dispatches on the
receiver individual’s actual committed type to the unique covering impl (<:-aware — an impl
for Account covers a SavingsAccount receiver). An ambiguous receiver — classified under two
<:-incomparable covered targets — and a receiver with no covering impl are both loud runtime
refusals naming the impls / the receiver’s types (Conformance), never an arbitrary pick. Invocation
flows through the standard mutation-dispatch surface (CLI harness / serve / SDK); the receiver is
passed as the self argument (an individual reference), exactly like any RFD 0019 entity-ref
parameter. fn members elaborate and dispatch the same way through the compute surface (Resolution has
the precise execution envelope). Mutations do not call mutations; member mutates do not change
that.
Generic parameters
Unbounded generics parse and travel the wire; bounded generics are rejected
(OE0667). The conditional-impl coherence theorem (Argon.TypeSystem.Conditional) is mechanized.
Resolution
Calling convention (rule plane). Trait::member(args) qualified is always legal. Bare
member(args) is legal when exactly one provider of that name and arity is in lexical scope;
ambiguity is an error naming the candidates. There is no postfix p.member() sugar in rule
bodies — p.x is field-access space (Rule-atom grammar).
The coverage gate. A bare member atom requires the argument’s static type to be fully
covered — you cannot call what isn’t implemented, statically. Coverage is computed over the
workspace-closed catalog via the abstract modifier (metatype, RFD 0027 D6): a static type T is
fully covered iff every instantiable (non-abstract) declared S ⊑ T (including T itself
when non-abstract) is ⊑ some impl target. Abstract types admit no direct instances
(OE0233 refuses construction) — every individual is classified through some non-abstract
subtype — so they need no covering themselves; their non-abstract descendants do. The
instantiability oracle is a pure wire-flag read (Argon.TypeSystem.Conformance. instantiableOfAbstract); no axis vocabulary is consulted — a sortality::non_sortal
binding is the package’s own ontology and exempts nothing by itself. The default is
instantiable: an unmodified declaration (the neutral std::core::type baseline, any
pub metatype m = { };) obligates coverage — the gate can only over-refuse, never silently
admit a vacuous member call, and the exemption is the explicit abstract opt-in. Partial
coverage is legal only under an explicit conformance guard:
pub derive CanVote(p: Person) :-
implements(meta(p), Adulthood), -- the visible branch
Adult(p), p.registered
Uncovered or unguarded-partial atoms are OE1327, whose help names the uncovered types and
offers both fixes (add the impl, or write the guard). Dispatch that can fail is always visible in
the source. Under multiple classification meta(p) is multi-valued (Reflection); the guard is
existential by construction — meta is a relation and the conjunction is a join, so the
guard holds iff some <:-minimal classifier of p is covered, and the clause that fires is
the one whose target that classifier satisfies.
Invocation-plane dispatch. Invocation-plane members are end to end through the execution surfaces Argon has — the dispatch surfaces, where the only type information is the receiver individual’s committed classification:
- A member is invoked by its callable name
Trait::member(qualified path, or any unambiguous::-suffix — the same resolution rules as mutations and rule heads; ambiguity is an error naming the candidates), or pinned to one impl by its per-impl identityTrait::member@Target. The pinned form is an explicit escape hatch: it bypasses receiver dispatch and does not re-verify that the receiver is covered by that impl’s target (parity with ordinary mutations, which do not re-verifyupdate x: Tagainstx’s classification). - The
selfargument carries the receiver individual; dispatch selects the unique impl whose target covers the receiver’s actual committed type (<:-aware). No covering impl, and a receiver under two<:-incomparable covered targets (the Conformance residual), are loud refusals naming the types / the impls — never an arbitrary pick. - mutate members run through every mutation surface (the CLI demo harness, serve’s
/v1/dispatch/mutation, the generated SDK’s member endpoint); fn members run through the compute surface (/v1/dispatch/compute, SDK) — exactly the surfaces where top-levelfndeclarations execute.
Source-level call sites are not an execution surface — for any fn. Top-level fn
declarations cannot be called from rule bodies or from fn/mutate bodies (term-position
user-function application does not resolve); method-call syntax x.foo() parses but has no
semantics — the call expression builds, resolves to nothing, and derives nothing (the same
silent-vacuity bucket as term-position application; tracked for a loud gate). fn
members inherit exactly that envelope — they are declared, conformance-checked, lowered, and
dispatchable wherever top-level fns are, and nowhere else. Source-level calls
bind to the rules this section reserves: build-time resolution (first x’s inherent impls, then
trait impls; one concrete body, no vtables), Trait1::foo(x) (UFCS) on collision, bare calls
legal where exactly one trait in scope provides the member, and the receiver’s static type
subject to the same coverage gate.
Supertraits are requires-constraints. pub trait Renewable: Resource makes
impl Renewable for Lease well-formed only if impl Resource for Lease exists (OE0674).
Nothing is inherited — no member bodies, no identity, no lattice position.
Orphan rule. An impl Trait for Type must live in the package declaring Trait or the one
declaring Type (OE0672), so every workspace’s impl set is unambiguous.
Conformance
Enforced at ox check and ox build over the closed catalog (elaboration-time diagnostics, not
check rules):
| Obligation | Diagnostic |
|---|---|
| Impl provides every declared member | OE0670 ImplMemberMissing |
| Impl member not declared by the trait / signature mismatch (incl. parameter-name mismatch on invocation-plane members — wire arguments bind by name across every impl of a callable) | OE0671 ImplMemberExtraneous |
| Orphan rule | OE0672 OrphanImplViolation |
Two impls of one trait with <:-comparable targets, or targets sharing a declared common descendant | OE0673 ImplTargetsOverlap |
| Supertrait impls present | OE0674 SupertraitUnsatisfied |
Self discipline (incl. the invocation-plane receiver rule: fn/mutate members lead with self) | OE0675 SelfMisuse |
| Genuinely unsupported member shapes: trait default bodies, inherent (bare-impl) members, unrecognized forms | OE1326 TraitMemberNotYet |
| Bare member atom over an uncovered type | OE1327 TraitMemberUncovered |
No-overlap (OE0673) instantiates the mechanized coherence contract by construction — a nominal
target is a TraitBound with satisfied T := T ⊑ target — so at most one impl covers any
declared instantiable type and there is never a specificity question. Both arms matter:
impl Adulthood for Person + impl Adulthood for USPerson is OE0673 (the “default + override”
pattern is rejected by design — write disjoint targets, or one impl whose body branches), and
impl T for Person + impl T for Customer is OE0673 when some pub type Employee <: Person, Customer is declared (the common descendant would sit under two clauses). One residual cannot be
closed statically — the set of an individual’s runtime classifications is not known at compile
time: an individual dynamically classified under two <:-incomparable
covered targets with no declared common descendant. Its semantics is defined, not accidental — in
the rule plane the clauses are independent sufficient conditions (both may fire; the union is
their disjunction); in the invocation plane an ambiguous receiver is a loud runtime refusal.
Reflective conformance: TraitRef and implements
TraitRefis a reflective sort parallel toTypeRef(RFD 0023): values are handles to declared traits (carrierValue::Name). It is a sibling sort underEntity— deliberately not on theMetatype <: TypeRefchain, because traits are not types.implements(t: TypeRef, tr: TraitRef) -> Boolis the fifth reflection intrinsic (Reflection), materialized as the RT-closed reserved-head relation$implementsfrom impl declarations, with supertrait closure and<:upward target-coverage folded in (an impl forPersoncoversUSPerson, World assumptions (CWA / OWA)-coherent).$implementsis catalog-closed, sonot implements(…)is stratification-safe, and — unlike the type-position arguments of the other four intrinsics — both positions ofimplementsare exempt from OE0212 and may be free: enumeration is relational (implements(t, Adulthood)with freetyields the implementing types;implements(meta(p), tr)with freetryields the traits ofp’s type). The expression plane gets the boolean form (if/matchscrutinees) andimplementors(Trait) -> Set<TypeRef>.- Conformance checks are catalog-level vocabulary under RFD 0025 D1 as amended by RFD 0026
(catalog-level iff every variable is reflective-sorted:
TypeRef,TraitRef, orMetatype), so they discharge statically atox check:
#[static]
pub check EveryPersonKindHasAdulthood(t: TypeRef) :-
specializes(t, Person),
not implements(t, Adulthood)
=> Diagnostic {
severity: Severity::Error,
code: "Onto::E001",
message: format!("{} must implement Adulthood", t),
}
Wire and Lean correspondence
Member rules are emitted as ordinary RuleDecl/QueryDecl events; invocation-plane members as
ordinary MutationDecl/ComputeDecl events keyed Trait::member@Target — no new AxiomKind.
TraitDeclBody.methods carries member signatures (all five planes); ImplDeclBody.items carries
member provenance (both fields carry it — the wire is built for this).
spec/lean/Argon/Substrate/Trait.lean carries the @[language_interface] shapes;
Argon.TypeSystem.Conditional states the coherence contract that OE0673 enforces. The Lean
obligations for this design (monomorphization conservativity on the Reasoning/Datalog/ spine;
coherence instantiation; structural-tier conformance) are listed in RFD 0026.
Modeling guidance: traits versus phases and categories
If it classifies, it is a concept; if it obligates behavior, it is a trait. “Adult” as a
classification is an anti-rigid phase of Person — model it as
pub type Adult <: Person iff { age >= 18 } and the substrate derives membership. Reach for a
trait when otherwise-unrelated types must satisfy a common contract of derivations or operations
(Closable, Auditable, per-jurisdiction rule families) — the contract is about what the types
provide, not what their instances are. A trait carrying a single unary Self-member named
like a classification is usually a phase in disguise.
Examples
-- Marker trait.
pub trait Renewable;
-- Rule-plane contract; per-jurisdiction clauses.
pub trait Adulthood {
derive Adult(Self)
}
impl Adulthood for USPerson { derive Adult(p: Self) :- p.age >= 18 }
impl Adulthood for GermanPerson { derive Adult(p: Self) :- p.age >= 18, p.has_residence }
pub derive CanVote(p: Person) :-
implements(meta(p), Adulthood), Adult(p), p.registered
-- Invocation-plane contract. The receiver leads; invocation passes
-- `self` (an individual ref) and dispatches on its actual type.
pub trait Closable {
mutate close(self, reason: String);
}
-- Supertrait as requires-constraint (Rust's `:`). Return-position
-- `Self` is OE0675 (it would need union types / bounded generics);
-- return a concrete type.
pub trait Repaintable: Drawable {
fn repaint_cost(self, color: Color) -> Int;
}
-- Bare impl (inherent grouping): parses per `impl Type { … }` — grouping, but its members are
-- OE1326-gated at elaboration — inherent members do not execute yet
-- (the RFD 0026 invocation plane covers trait members only).
impl Lease {
fn extend(self, days: Nat) -> Lease;
}
Macro atom
The macro atom is the fifth substrate atom (meta-calculus, constructs, rule, trait, macro) and the extensibility layer the ontology-neutral doctrine depends on: it is how a vocabulary library grows the surface without a compiler change. Its design is settled by RFD 0037; this chapter is the surface specification.
The declarative expansion engine — pub macro declarations expand at EXPAND (hygiene, repetition, attribute/decl-position invocation, cross-module imports), and their output re-elaborates through the ordinary path to events. The procedural layer (#[procmacro] pub fn, Procedural macros) provides concat_idents, quote/${} antiquotation, decl-reflection, and a total non-recursive body — the mechanism that re-homes #[irreflexive]/#[asymmetric] from compiler builtins to library procedural macros. #[functional] is a builtin-backed macro (Migration: re-homing the hard-coded surface).
The shape of the design. A macro expands to surface syntax, which is re-parsed and re-elaborated through the one existing path to events — never to events directly. Expansion is a phase between parse and resolve, run to a fixed point; the resolver, type checker, Name resolution ontology-neutrality gate, and decidability-tier classifier all run on the post-expansion program, so a macro is invisible to none of them. The declarative layer (pub macro, pattern→template) is the foundation; the procedural layer (#[procmacro], computation over reflected syntax) is layered on top (Procedural macros).
Declarative macros
macro-decl ::= attribute* 'pub'? 'macro' Ident '{' macro-rule (';' macro-rule)* ';'? '}'
macro-rule ::= '(' macro-pattern ')' '=>' '{' template '}'
macro-pattern ::= /* token tree with $name:SPEC metavariables and $(...)* / $(...)+ / $(...)? repetitions */
A declarative macro is a sequence of (pattern) => { template } rules. A pattern matches a token tree; metavariables are tagged with a fragment specifier that fixes the syntactic category matched and the binding space the bound name lives in. The template is surface syntax, with metavariable substitution and $( … )*/+/? repetitions (optional separator: $( … ),*).
pub macro vec {
() => { Vec::empty() };
($head:expr $(, $tail:expr)*) => { /* … */ };
}
let xs = vec!(1, 2, 3);
Fragment specifiers. The syntactic categories $x:expr, $t:ty, $i:ident, pat, stmt, block, item, literal, path, tt; plus four Argon-specific specifiers, each a binding-space-targeted parse — $c:concept parses a concept-introducing declaration and binds in the concept space, $r:rel a relation declaration (rel space), $m:metatype a metatype introducer, and $u:rule a rule-body atom (carrying its own rule-variable sub-bindings). The set is closed; a standpoint specifier and a user-extensible registry are out of scope (Out of scope). Because a concept/metatype specifier only binds — the Name resolution gate runs later, at resolution (Hygiene) — and no specifier carries a tier, ontology-neutrality and tier-honesty hold by construction.
The expansion model
Expansion is a distinct compilation phase:
lex → parse → expand* → resolve → check → instantiate (lower to events) → tier-classify → discharge
- Expand to surface, re-elaborate. A macro produces surface syntax that is spliced into the module and re-parsed; it then flows through the same resolve → check → lower path as hand-written source. The content-addressed
AxiomKindevent wire is produced only by the unchanged lowering, so a macro inherits the substrate’s identity and drift guarantees for free. A macro never emits events directly. - Fixed point. A macro whose output contains further invocations re-expands until none remain, bounded by a fuel cap that errors on exhaustion.
- Emit into the invocation module. A macro expands at its use site and its output lands in the invoking module, where rules it emits compose with the user’s by the same-module same-head union (
derive). It cannot contribute to another module’s heads. - Classifier-last gives tier-honesty. The decidability-tier classifier runs after expansion on the lowered events, so the program lands on its true tier — a macro that emits a recursive rule is classified exactly as if the rule were hand-written, and cannot smuggle the program across a tier boundary.
- Determinism. The declarative layer is strongly normalizing by construction, has no I/O, and emits collections in a canonical order. Rule variables are alpha-canonicalized at lowering (Hygiene), so a macro’s choice of fresh variable names never affects the
.oxbincontent hash. Expansion is therefore a deterministic function of its input, and reproducible builds need no author discipline.
Hygiene
Macro-introduced binders neither capture nor are captured by names at the use site, across all of Argon’s binder namespaces (rule variables, concept/type, rel/metarel, metatype/metaxis + axis values, individuals, trait members). The model is scope-sets with binding spaces (Flatt 2016 + Racket binding spaces): each namespace is an interned scope inside one scope-set, resolved by the unchanged maximal-subset rule. Hygiene is white-box — the quotation applies a fresh macro scope to the identifiers a macro introduces.
The Name resolution ontology-neutrality gate composes as a commuting pass: scope-set resolution (syntactic) produces a candidate binding, then the gate (semantic) checks that a macro-introduced concept resolves to a visible pub metatype — it may reject, but never re-points a reference. Macro-introduced vocabulary is not exempt from the gate.
Two regimes follow from what reaches event identity:
- Rule variables are clause-local and alpha-canonicalized at lowering, so hygiene need only guarantee non-capture; their names are irrelevant to identity.
- Introduced vocabulary (a concept/relation a macro declares) has a semantic, referenceable name, so hygiene gives it a content-derived, stable identity.
Macros are hygienic-only: there is no unhygienic!. A $crate-style self-reference plus syntax-parameter keywords cover the real needs; any future raw escape must name the binding space it breaks into.
Imports and the shared #[…] namespace
A pub macro foo { … } declaration occupies a single symbol in the module namespace. use mod::foo; brings it into scope; foo!(args) invokes it at expression position and #[foo(args)] at declaration position. The ! and #[…] sigils are invocation markers, not separate-namespace tags — there is no distinct macro namespace, and no #[macro_use] form; macro imports follow ordinary item-import rules (Imports).
User macros and compiler-known directives share the #[…] namespace. Resolution is by uniqueness at registration: a name resolves to exactly one implementation, and a user macro colliding with a built-in directive is an OE0705-class error, never a silent shadow (matching the loud-refusal discipline of the directive registry, Directive surface and the MLT decorator path). Identity is keyed to a stable handle, not the spelling, so re-homing a directive (Migration: re-homing the hard-coded surface) does not change how a #[name] resolves.
Procedural macros
A procedural macro computes its expansion by running code, rather than rewriting a pattern into a template. It is not a fn over a TokenStream: the procedural layer is a total, structurally-recursive meta-language over reflected syntax, not general-purpose computation. Totality is what the substrate’s own doctrine requires — it is strongly normalizing and deterministic by construction, and (being a total function over an inductive syntax type) mechanizable in Lean (Lean correspondence).
A procedural macro is a #[procmacro] pub fn in the one macro namespace:
#[procmacro]
pub fn irreflexive(item: Decl) -> Syntax {
let head = concat_idents("__", item.name, "_irreflexive");
quote {
$item;
pub check $head(x: $item.params[0].ty) :- $item.name(x, x)
=> Diagnostic { severity: Severity::Error, code: "Argon::OE1359",
message: "relation `${item.name}` is #[irreflexive] but relates an element to itself" };
}
}
The body is a total, non-recursive fragment: let bindings over the construction builtins and reflection projections, then a trailing quote. The surface is:
quote { … }— aQUOTE_EXPR; its body is re-parsed through the ordinary elaboration path (never emitted as events directly). Splices:$name(alet-bound value),$item(the whole reflected declaration, re-emitted verbatim),$item.name(the relation’s name),$item.params[i].ty(a parameter’s type).${expr}antiquotation — an in-string splice (e.g. inside a diagnosticmessage:), resolved by the macro renderer at EXPAND. It is render-time, not runtime string concatenation (the runtime has noText + Text— that is a separate arc).concat_idents(a, b, …)(paste) — builds a raw definition-site identifier from pieces; the single capability the splice-only declarative layer lacks. The result is not freshened — the macro intends it as the stable public head (__{rel}_{prop}); splices are substitution barriers carrying their own use-site identity.
Bodies are non-recursive (trivially total). #[procmacro] resolves as a macro (DefKind::Macro), runs at EXPAND by a dedicated evaluator, and its #[procmacro] directive executes at fn-decl position. #[irreflexive] and #[asymmetric] (Migration: re-homing the hard-coded surface) are genuine procedural macros written exactly this way.
Full structural recursion over reflected Syntax — a totality checker, a derive-class macro inspecting a concept’s fields, #[functional]’s rel-cardinality-cap arm (which re-emits a rel with a modified cardinality bracket), and the classify ∘ expand tier-honesty theorem — is out of scope (Out of scope).
Directive surface and the MLT decorator path
Every #[…] attribute must resolve to a registered compiler directive or an in-scope pub macro (e.g. #[transitive], re-homed to std::rel, Migration: re-homing the hard-coded surface / RFD 0037 D8 — brought in by use). The directive registry (oxc-instantiate’s DIRECTIVE_REGISTRY) is the single declarative table of every directive the toolchain reads or reserves, with its argument shape, valid positions, and status. There are no silently-ignored attributes: an attribute that is neither a registered directive nor an in-scope macro refuses with OE0705 UnknownDirective (nearest-known suggestion), documented-but-unbuilt directives with OE0706 DirectiveReservedUnimplemented, misplaced directives with OE0707 DirectiveInvalidPosition, argument forms on bare-form directives with OE0709 AttributeArgsNotYetImplemented, and conflicting families with OE0714 DirectiveConflict.
The relation-property directives are library macros, split by mechanism (Migration: re-homing the hard-coded surface):
- Relation-property directives (RFD 0031) —
#[transitive]is a declarativepub macroinstd::rel(closure deriveR(x, z) :- R(x, y), R(y, z)into the relation’s own head);#[irreflexive]/#[asymmetric]are genuine procedural macros (#[procmacro], RFD 0040) that re-emit the decorated declaration and paste a__{rel}_{prop}check head viaconcat_idents(Procedural macros);#[functional]is a builtin-backed macro (#[builtin] pub macro, the#[rustc_builtin_macro]analogue,OE1362if unimported), since its rel-cardinality-cap[0..1]arm requires structural re-emission. The declarative and procedural forms format Argon source and re-parse it through the ordinary derive/check pipeline;#[functional]’s privileged path synthesizes the guard (and the cap) directly. - MLT decorators —
#[categorizes(T)],#[partitions(T)],#[subordinate_to(M)],#[power_type_of(T)], and#[order(N)]are no longer compiler directives at all. They are genuine library#[procmacro]s in the unprivilegedmltpackage (notstd): each re-emits the decorated declaration and emits aMetaPropertyon the metarel (or, for#[order], the metaxis) it shares its name with — reading the argument’s name (or, for#[order], an integer), not its structure. Nothing is ambient: a consumer listsmltinox.tomland imports the vocabulary (use mlt::*;/use mlt::{categorizes};), and an unimported decorator is an unknown attribute macro, refused by the no-silent-attribute gate (OE0705). The emittedMetaPropertyis re-checked at the emission boundary and again at load —#[order]’s value is validated against themlt::ordermetaxis’s declaredNat where { _ <= 16 }domain by the self-validating.oxbinload re-check (OE0623), so the bound holds without any compiler privilege. The reasoner-side enforcement of the MLT closure rules over the emitted metaproperties is authored as themltpackage’s own rules/checks over the neutral substrate (Phase 2).
Directives as library macros
The hard-coded directive surface is library macros, with the user-facing syntax byte-identical to a privileged-path synthesis (RFD 0009 RP-003 GAP-3). The mechanism is decided by the directive’s emit target:
- Relation-property emits ordinary rules (which have surface), so it lives in
std::rel, split by what each member needs:#[transitive]is a declarativepub macro: the macro template produces lowered events byte-identical to a direct synthesis (verified differentially).#[irreflexive]/#[asymmetric]are genuine procedural macros (#[procmacro] pub fn … -> Syntax, RFD 0040): each re-emits the decorated declaration ($item) and pastes apub checkwhose head isconcat_idents("__", item.name, "_{prop}")— the unique ident the splice-only declarative layer cannot build. Their import gate isOE0705(a bare unimported#[irreflexive]is an unknown attribute macro, liketransitive).#[functional]is a#[builtin]macro (importable fromstd::rel,OE1362if not), with privileged synthesis as the implementation. It is one attribute covering both ametarel-check (paste-able) and arel-cardinality-cap[0..1]on the target endpoint — and the cap arm re-emits arelwith a modified cardinality bracket, i.e. structural re-emission. An attribute name is builtin or procmacro, not split across both halves, sofunctionalis entirely builtin.
- MLT emits
MetaPropertyevents. Its surface is the unprivilegedmltpackage (RFD 0043): every decorator — the relational#[categorizes]/#[partitions]/#[subordinate_to]/#[power_type_of]and the metaxis-valued#[order(N)]— is a genuine#[procmacro]emitting a re-checkedMetaPropertythrough the emission boundary (Procedural macros). There is no compiler builtin, nostd::mlt, and no reserved MLT diagnostic code; the expander is the package’s own procmacro, not a privileged compiler path. MLT is one higher-order type theory among several (potency, ML2, powertype), so the substrate stays neutral about which one a model commits to. std::temporal/std::lifecyclebecome declarative library macros; whether each operator ships native or library is a scoping choice once the engine exists.
// std/temporal.ar — a declarative desugaring to rule-body atoms (no computation)
pub macro since {
( $lhs:rule since [ $lo:literal , $hi:literal ] $rhs:rule ) => {
@meta_window($t, $lo, $hi), $rhs at $t, holds_throughout($lhs, $t, now)
};
}
Lean correspondence
The substrate at spec/lean/Argon/Substrate/Macro.lean mechanizes the shape of macro declarations — MacroAtom = declarative MacroDecl | procedural ProcMacroDecl as @[language_interface] carriers policed by the drift gate. Per RFD 0037 D7, the substrate boundary is held at the typed AST: macro expansion is untrusted and Rust-side, and assurance comes from re-checking its output — the tier classifier runs last, the content hash is re-derivable, and the drift gate covers the MacroAtom shape. Mechanize the re-checker, not the producer.
The tier-honesty theorem — that classify ∘ expand lands a program on the same decidability class as its expansion — is statable only once expand is a total Lean object over an inductive Syntax type (RFD 0040 D7). The scope-set hygiene algebra and a full expansion-preservation theorem are out of scope.
The MLT-relational kinds — instanceOf, specializes, categorizes, partitions, subordinates — live in spec/lean/Argon/MetaCalculus/MLTKinds.lean, with the round-trip theorem classify_declOfKind; the elaborator emits canonical metarel_decl events via declOfKind.
Testing
A test declares an in-language unit test: a named imperative block run against a fresh world by ox test. Its name is a string literal; its body is the mutate-body statement set (mutate) plus the assert statement. An assert mirrors require, except a failed assert records a pass/fail outcome and execution continues (rather than aborting). Three assertion forms exist: the value/boolean form assert <expr>;; the derivability form assert [not] derivable F(args);, which tests membership / non-derivability of a fact against the materialized extent with world-honest three-valued outcomes — present ⇒ PASS/FAIL, absent under closed-world ⇒ FAIL/PASS, absent under open-world ⇒ a loud INCONCLUSIVE (absence is unknown, not false, World assumptions (CWA / OWA)); and the rejection form assert rejects [( Pkg::Code )] { … }, which asserts that a write block is refused by a write-path guard (a where-invariant, a check delta-guard, a group axiom, …) — a genuine guard rejection PASSes (matching a pinned code if given), an accepted write FAILs, and a non-guard error ERRORs loudly so a broken test never masquerades as a passing rejection. derivable and rejects are contextual keywords (leading position only). The runner, the full assertion semantics — including reads over the deductive plane, the three-valued tables, and the guard-vs-non-guard classification — isolation, and the scope are specified in Runtime contract.
test-decl ::= attribute* 'test' String '{' test-stmt* '}'
test-stmt ::= mutate-stmt // `mutate`: let / insert / update / delete / for / require / calls
| 'assert' expr ';'
| 'assert' 'not'? 'derivable' IDENT '(' (expr (',' expr)*)? ')' ';'
| 'assert' 'rejects' ('(' path ')')? '{' mutate-stmt* '}' ';'
(The fixture / expect block forms are reserved and do not parse.)
pub type Account { name: String }
// A test constructs a world top-to-bottom and asserts over it; `ox test`
// runs each test against its own fresh store and reports PASS / FAIL / ERROR.
test "a constructed account carries its name" {
let cash = insert Account { name: "Cash" };
assert cash.name == "Cash";
}
Discovery. ox test runs test declarations from mod-reachable modules and auto-discovers every *.ar anywhere under the package’s tests/ directory (recursive, any depth — so no test under tests/ silently fails to run), elaborating them as part of the package — so a test placed in the reserved tests/ directory runs without mod-wiring, symmetric with scenarios/*.toml. This is test-mode only: tests/*.ar never enters ox build’s artifact. See Runtime contract.
Isolation. Each test runs against its own fresh store seeded with the package’s declared facts; no test sees another’s writes, so test ordering is unspecified and irrelevant. Within a test, the body’s writes are visible to later assertions, including the facts they derive (read-your-writes over the committed + deductive state). See Runtime contract for the ox test runner and the assertion semantics in full.
Scenario harness — ox run-scenario
An .ar package declares the ontology; it does not carry data. So a built .oxbin queried with no fixtures answers against an empty store. A scenario is a small .toml script that seeds the store and then reads it back, so a model ships with realistic, reproducible state — and, with assertions, with executable checks.
A scenario file lists an ordered sequence of steps. Each [[step]] is tagged by do:
[[step]]
do = "mutate" # invoke a declared mutation
path = "norms::register"
args = { p = "alice", age = 25 }
[[step]]
do = "query" # run a declared query, print its rows
path = "norms::findEmployeeNamesOfCompany"
args = { c = "acme" }
label = "employees of acme" # optional, echoed in the output
[[step]]
do = "derive" # dump a derived predicate's extent
name = "spouses" # the rule head's short name
explain = true # optional: annotate each tuple with its proof tag
[[step]]
do = "compute" # invoke a declared `fn`, print its single result
path = "norms::getSpouse"
args = { p = "alice" }
Document order is execution order. A scenario interleaves writes and reads: populate, look, mutate again, look again — each read sees the state at that point. The four kinds map to the same operations as the standalone commands: a mutate step runs a declared mutation (with the same check-guard handling as a write over HTTP); a query step runs a declared pub query and prints its rows; a derive step dumps a derivation’s extent by its rule-head short name, the read ox derive <oxbin> <rule> performs (with explain = true adding the Rule atom — fn, derive, query, mutate, check proof tag per tuple); a compute step evaluates a declared pub fn and prints the single value it returns.
A query step’s args is what makes a parameterized declared query do a keyed lookup. A pub query findEmployeeNamesOfCompany(c: Company) -> … whose body binds c looks c up only when c is supplied; with no args, the parameter degrades to a free join variable and the query enumerates the whole extent. A derive step takes no args — a derivation has no parameters, so it always dumps the full extent.
Argument typing
Each args table maps a parameter name to a TOML value, typed into the runtime’s value space by one shared rule (the same path expect tuples use, below):
| TOML form | Runtime value |
|---|---|
bare string "alice" | an individual (hashed by name, identical to pub fact P(alice);) |
{ individual = "alice" } | the same individual (explicit form) |
{ text = "active" } | a String value (use this for a string-typed field, since a bare string is an individual) |
integer 25 | Int |
boolean true | Bool |
{ decimal = "0.22" } / { money = "100.50" } / { real = "…" }, or a bare float 0.22 | an exact Decimal / Money / Real, parsed exactly (never via f64) |
{ date = "2024-01-01" } | a Date |
A bare string defaulting to an individual is the load-bearing convention: the same name in the source and in the scenario refers to the same entity. Array values and a non-finite float are rejected with a clear diagnostic.
Assertions — expect
Add an expect table to a step and it stops being a printer and becomes a test: its result is diffed against the expectation and a PASS/FAIL verdict is reported. Which sub-keys apply depends on the step kind:
[[step]]
do = "query"
path = "norms::findEmployeeNamesOfCompany"
args = { c = "acme" }
expect = { equals = [ [{ text = "Dave" }], [{ text = "Alice" }] ] }
[[step]]
do = "derive"
name = "isDisenfranchised"
expect = { empty = true }
[[step]]
do = "compute"
path = "norms::getSpouse"
args = { p = "alice" }
expect = { value = "dave" } # a single expected value
[[step]]
do = "mutate"
path = "norms::register"
args = { p = "bob", age = 12 }
expect = { rejected = "Norms::E001" } # assert a check guard rejects it
- A
queryorderivestep takes the row-set sub-keys:rows = N(exact count),contains = [tuple, …](every listed tuple is present, count-sensitive),equals = [tuple, …](the result set matches exactly, order-insensitive multiset equality),empty = true(no rows). - A
computestep takesvalue = <toml value>: a single expected value. - A
mutatestep takesrejected = "<CODE>": assert the mutation is rejected by acheckguard carrying that user code. A rejection by that code is a PASS and the run continues (the rejected transaction committed nothing); a success or a rejection by a different code is a FAIL. Withoutrejected, a guard rejection halts the scenario.
The correctness crux is that an expected value is typed through the same rule as the step args, and the diff is by value identity — never by rendered string. So an entity-valued result column matches a bare-string or { individual = "…" } expectation, but never a { text = "…" } one; a Text "dave" is never equal to the individual dave. A sub-key applied to the wrong kind (value on a query, rows on a compute, an unknown key) is a loud scenario error, not a silent no-op.
Files, discovery, and running
A package carries scenarios as a sibling demo.toml and/or any number of files under a scenarios/ subdirectory:
ox run-scenario <pkg> # run every discovered scenario
ox run-scenario <pkg> --scenario voting # run scenarios/voting.toml (or a sibling voting.toml)
ox run-scenario <pkg> --scenario path/to.toml
ox run-scenario <pkg> --extent norms::Voter # also print a concept extent after the run
With no --scenario, run-scenario discovers every scenario — the sibling demo.toml plus every scenarios/*.toml, filename-sorted for determinism — and runs them all. Each scenario runs against its own freshly-loaded, freshly-seeded store re-derived from the artifact bytes, so a mutation in one scenario cannot leak into another’s reads and the order across files is irrelevant. --scenario runs exactly one: a path verbatim, or a bare name resolving to scenarios/<name>.toml then a sibling <name>.toml. --extent <concept> prints that concept’s extent once the scenario has run.
run-scenario collects every failed assertion (it does not stop at the first) and exits non-zero if any assertion — or any scenario — failed, naming the offending file. A scenario therefore doubles as a CI-usable check.
A single file uses either [[step]] or the older flat [[mutate]] list (a pure population script, applied in order), never both — the whole point of [[step]] is interleaving, which a split across two arrays cannot express. The flat [[mutate]] form is also what ox query and ox derive apply when they find a sibling demo.toml: they seed from [[mutate]] fixtures, then run their own read. A [[step]] scenario beside one of those commands is not run by them (they have nowhere to surface the interleaved reads); they print a warning pointing at ox run-scenario.
The scenarios/ directory is the integration-.toml surface, distinct from tests/, which holds the in-language test atom (The test atom — ox test). The two are complementary: scenarios/ scripts the runtime from outside the language with a .toml harness; the test atom expresses checks inside the language.
The test atom — ox test
A test is a named imperative block, run top-to-bottom against a fresh store. Its name is a string literal; its body is the mutate-body statement set (let / insert / update / delete / for / require and declared mutation/fn calls — mutate, RFD 0015) plus the assert statement:
test "accountBalance derives the posted amount per account" {
let cash = insert Account { name: "Cash" };
let rev = insert Account { name: "Revenue" };
let e = insert Entry { memo: "sale 1" };
postPair(cash, rev, cash, rev, e, 100.50);
assert accountBalance(cash) == 100.50; // reads the derived value
assert accountBalance(rev) == -100.50;
}
An assert mirrors require exactly, with one difference: a failed require aborts the body; a failed assert records a pass/fail outcome and execution continues. true is a PASS, false a FAIL, and an evaluation error (an unbound reference, a non-boolean result, a missing field, a value not derivable in the current state) is an ERROR (the test errors; it never panics). Setup and assertions interleave naturally, so there is no separate fixture block.
Asserting over the deductive plane. A condition that names a derived predicate or a declared pub query — accountBalance(cash), or the nav-method form x.active_leases() — is evaluated against the reasoner’s materialized extent at the fixpoint of the current committed state, the same read path ox query / ox derive and the for x in <derive> snapshot use. A keyed call (accountBalance(cash)) selects the row whose leading columns match the argument and returns its head-carried value; with no matching row the assert ERRORs (the value is not derivable), and an ambiguous match is a loud error too. Scalar comparisons, field access, arithmetic, and aggregates evaluate as in a require guard. This is the point of testing a reasoning system: an assert checks what the rules derive, not just what was stored.
Asserting derivability — assert [not] derivable F(args). The value-read assert above answers what value a keyed derive carries; the derivability form answers whether a fact holds at all — membership and, crucially, non-derivability. assert derivable F(a, …) passes when a row whose leading columns equal the resolved args is in F’s materialized extent; assert not derivable F(a, …) is its negation. The predicate F resolves the same way the value-read assert and for x in <derive> do — a derive head, a declared pub query, or a base rel. The predicate slot accepts a qualified path (assert derivable students::adult(p), assert not derivable pkg::accounts::adult(p)), resolved by the same ::-suffix matching the rest of the runtime uses: a bare leaf resolves when it is unambiguous, and qualifying disambiguates a name carried by two heads in different modules — that case stays a loud ERROR for the bare form, naming every candidate, so qualification is the way to pick one. Partial args test prefix membership (derivable amtOf(acme) over amtOf(account, money) = “some row has account == acme”); zero args test extent non-emptiness (derivable F()). A predicate that names nothing is a loud ERROR, not a silent outcome. derivable is a contextual keyword — recognized only in leading position after assert / assert not, so it stays usable as an ordinary identifier everywhere else.
The outcome is world-honest and three-valued, keyed on the predicate’s world assumption (World assumptions (CWA / OWA)):
| extent state | assert derivable F | assert not derivable F |
|---|---|---|
a matching row is present (the fact is) | PASS | FAIL |
absent, F is closed-world (absence ⇒ not) | FAIL | PASS |
absent, F is open-world (absence ⇒ unknown) | INCONCLUSIVE | INCONCLUSIVE |
The open-world row is the load-bearing soundness case. Under the open-world assumption absence of evidence is not evidence of absence (World assumptions (CWA / OWA)): a missing row is unknown, not false, so closed-world non-derivability is not assertable and must never silently pass. The runner reports a distinct, loud INCONCLUSIVE outcome — neither PASS nor FAIL — carrying a teaching detail that names the open-world cause and the two ways forward (assert a positive outcome instead, or mark the concept #[world(closed)] if it should be closed). This composes with the world-honest negation-as-failure of the rule engine: a derivability assert reads the same world each negated atom does.
Asserting rejection — assert rejects [( Pkg::Code )] { … }. Argon is a constraint language; a model’s most important property is which writes its where-invariants and check delta-guards turn away. The two forms above assert positive outcomes — the negative-enforcement half is assert rejects { <mutate-body-stmts> }, which asserts that the block’s write is refused by a write-path guard. The block admits the full mutate-body statement set (insert / insert iof / update / delete / mutation calls); it runs against an isolated copy of the test world, so it commits nothing back regardless of outcome. An optional code pin assert rejects(Pkg::Code) { … } requires the rejection to carry that exact diagnostic code. Like derivable, rejects is a contextual keyword — recognized only in leading position after assert — so it stays usable as an ordinary identifier elsewhere.
The outcome is three-valued, and the guard-vs-non-guard distinction is load-bearing:
| block outcome | assert rejects | assert rejects(Code) |
|---|---|---|
| a write-path guard refuses the write | PASS | PASS iff the rejection carries Code, else FAIL (wrong reason) |
| the block commits with no rejection | FAIL (the write was accepted) | FAIL (the write was accepted) |
| a non-guard error (unbound var, type error, unevaluable) | ERROR | ERROR |
A guard rejection is a genuine constraint refusal on the write path — a where-invariant violation (OE0668), a check delta-guard refusal (which is also how a group axiom — disjoint / complete / partition — surfaces, carrying the check’s own Pkg::Code), an insert iof onto a defined (iff) concept (OE0211), a write onto an abstract or fixed-introduced type (OE0233 / OE0234), a construction-completeness refusal (OE0207 / OE1014), a property write to an unclassified individual (OE0238), or a relation write over a non-existent endpoint or past a declared cardinality (OE0232 / OE1341). Only these satisfy rejects. A non-guard error — a typo, an unbound reference, a type error, an unevaluable term, a stray require failure — is reported as a loud ERROR, never counted as a rejection: a broken test must never masquerade as a passing rejection test. An accepted write is a FAIL (and is rolled back, like any other block outcome, so it does not pollute the rest of the test). Use the positive forms to test that a legal write is accepted.
Isolation. Each test runs against its own fresh store seeded with the package’s declared facts (the same fresh-store-per-run discipline as the scenario harness, Scenario harness — ox run-scenario). No test sees another’s writes, so tests are order-independent. Within one test, the body’s writes are flushed and the read-model maintained before each assert, so an assert observes every earlier write — including the derived facts they entail (read-your-writes over the committed + deductive state).
Discovery + running. ox test runs every test from two sources, unioned: (1) any test declaration in a mod-reachable module (e.g. tests/mod.ar reached by mod tests;), and (2) every *.ar anywhere under <pkg>/tests/, which ox test auto-discovers (recursive, any depth — a nested tests/sub/foo.ar is discovered too, so no test under tests/ silently fails to run — filename-sorted) and elaborates as part of the package, so each sees the package’s full vocabulary. This mirrors how ox run-scenario auto-discovers scenarios/*.toml: a test placed in the reserved tests/ directory runs without mod-wiring. A tests/ file that is also mod-reachable is folded exactly once (its tests never double-run). A discovered tests/foo.ar references package items by their package-absolute path (pkg::…). This discovery is test-mode only: ox build / ox check and the shipped .oxbin never include tests/*.ar — tests do not ship. Tests are inert at query / serve time; only ox test enumerates and runs them:
ox test <pkg> # build the package (+ tests/), run every test, report
ox test <pkg> --filter NAME # run only tests whose name/path contains NAME
A tests/*.ar with a parse or resolve error fails loudly (ox test aborts with the diagnostic), never a silent skip; a package with genuinely no tests anywhere prints “no tests found” and exits 0.
ox test builds the package (so the run reflects current source), runs every discovered test, and prints a PASS / FAIL / ERROR / INCONCLUSIVE line per test — a failed or inconclusive assertion shows its rendered source text and why it could not pass — followed by an N passed, M failed, K errored, I inconclusive summary. A test’s rollup status takes the strongest of its assertions in the precedence Error > Fail > Inconclusive > Pass. It exits non-zero if any test fails, errors, or is inconclusive (a test you cannot decide is not green), so ox test doubles as a CI-usable gate (like ox run-scenario).
Assertion forms. Three forms are admitted. The value/boolean form is assert <bool-expr>; — scalar comparisons (==, !=, <, <=, >, >=), field access on bound entities (x.field, chained x.a.b), arithmetic on those, the aggregate/comprehension forms, and the deductive-plane reads above (a derived-predicate / pub query keyed call, and the nav-method x.method() form). The derivability form is assert [not] derivable F(args); with the world-honest three-valued semantics above. The rejection form is assert rejects [( Pkg::Code )] { <mutate-body-stmts> } — the negative-enforcement half above. All three are test-only: a stray assert (any form) in a mutate / fn body lowers to an unsupported form and ox check / ox build refuse it up front with OE1318 (the runtime also rejects it, as defense-in-depth) — it is never a silent no-op.
Only the three assert forms above are admitted; anything else is a loud refusal, never a silent pass.
Truth values
Types
Two related types, exported from std::core and in the prelude:
pub enum Truth4 { Is, Not, Can, Both } // substrate, no payload
pub enum Truth4Of<T> { Is(T), Not, Can, Both(T, T) } // parameterized lift
The unparameterized Truth4 is the substrate bilattice carrier — what the Lean spec mechanizes and what Federation.infoJoin, Consistency.append, and Projection.project operate over. The parameterized Truth4Of<T> is the stdlib presentation type used at query-result boundaries that have already produced typed projections — Is(T) carries the witness for the is verdict; Both(T, T) carries the two disagreeing witnesses.
Locked-substrate semantics (per Foundation/Truth4.lean, Standpoint/Consistency.lean, Standpoint/Federation.lean):
- K3 single-standpoint queries return projections containing only
Is,Not,Can. - FDE
across [...]queries may returnBoth(a, b)when standpoints disagree. #[consistency(paraconsistent)]standpoints admitBothinternally.
Default visibility and K3 projection
Default query return type is the projection type (e.g., Bool, [Lease]); Truth4Of<T> is introduced only when:
- The query has
across [...]. - The return type is explicitly
Truth4Of<…>. - A paraconsistent standpoint admits internal
Both.
K3 fail-closed projection. When a single-standpoint K3-context query projects to a non-Truth4 type, Can is folded into the projection per Pietz–Rivieccio Exactly-True semantics:
- Boolean projections:
Can→false. OnlyIs(true)is designated. - Set / list / record projections:
Can-valued cells are omitted from the result set. - Scalar projections:
Cancollapses fail-closed (omitted for collection-shaped results,falsefor Boolean predicates).
To preserve the distinction between Not and Can at the surface, declare the return type as Truth4Of<T> explicitly.
The compiler emits Info-level diagnostics on every across query reminding the modeler that Both may arise.
World assumptions (CWA / OWA)
A world assumption governs how the absence of evidence is interpreted:
- Closed (CWA) — if a fact isn’t derivable, it’s false. Negation-as-failure is total.
- Open (OWA) — if a fact isn’t derivable, it’s unknown. Negation requires positive disproof.
The blessed default is CWA; OWA is a live per-concept opt-in. Every concept Argon evaluates is closed-world unless it carries a
#[world(open)]mark: negation-as-failure over the stratified / well-founded fixpoint (Rule atom —fn,derive,query,mutate,check, truth values) treats an unmarked concept’s absent membership as definite-false. The package-leveldefault_world(CWA unless a manifest sets it otherwise) governs every concept without an explicit mark. This default is a deliberate choice: an Argon program is a data system whose extents are the authority for what is true, and CWA is the regime that makes “not in the extent” mean “false”. A modeler who wants the OWL/DL open-world reading for a specific concept opts that concept in with#[world(open)], described below (RFD 0045, #787/#796).
CWA is the operative default because Argon’s primary use is application data — counts, payments, schedules, classifications — where the stored extent is the closed world. Concepts that model genuinely-incomplete knowledge (where “absent” means “unknown”, not “false”) opt into the open-world reading per the surface below.
Per-concept OWA opt-in (#[world(open|closed)]) — live. The surface is a #[world(...)] attribute on an ontologically-classified concept:
#[world(open)]
pub type LegalAgent { mut name: String } // OWA — `unknown` is distinct from `false`
The world directive is executed (RFD 0045 D2): the elaborator reads the policy word — open or closed, any other word is a loud refusal — records the concept’s world assumption on the per-concept world map (keyed by qualified concept path), and the reasoner stamps that map onto every evaluation catalog so each negated concept-membership atom is read under its own concept’s world. The attribute is concept-only (OE0707 on other placements). Concepts without the attribute inherit the package default_world (CWA by default).
The OWA semantics. The world a concept declares changes how the substrate reads its absence:
Interaction with negation-as-failure (Rule atom — fn, derive, query, mutate, check). Under CWA, not P succeeds whenever P is not derivable. Under OWA, an absent P(x) is unknown, not a definite not — so a closed-world not H whose derivation depends on an open-world negation does not get to read that unknown as false. Rather than silently over-asserting the head, the substrate refuses that genuinely-unsound shape at ox check / ox build (OE1367 NegatedOpenWorldDerived): mark H’s concept open-world too (so not H tolerates the unknown), or restructure so the open-world negation does not flow into a closed-world-negated head. This is world-honest negation-as-failure: the engine honors default_world and each concept’s mark instead of evaluating closed-world unconditionally. The is unknown rule atom (Rule-atom grammar) is reachable under OWA — an open-world concept’s absent membership flows through it as unknown.
Interaction with the write side. A concept’s world governs its compiler-synthesized structural checks. Under the closed-world default a complete/partition covering check fires when no member membership is derivable (OE0241) and a disjoint overlap check refuses a double-classification (OE0240). Under an open-world concept the covering check softens to refuse-on-K3-not — refuse only on a definite violation, tolerating the merely-unknown case — while disjointness stays world-invariant (a positive overlap is definite under any world). This is the RFD 0045 D1/D2 design.
Interaction with the test atom. assert [not] derivable F(x) (Runtime contract) reads F’s world directly: a present row is PASS/FAIL; an absent row under CWA is definite non-derivability (FAIL for derivable, PASS for not derivable); an absent row under OWA is unknown, so closed-world non-derivability is not assertable — the runner reports a distinct, loud INCONCLUSIVE, never a silent pass.
Interaction with refinement (Refinement). The world a concept declares makes iff-derived membership three-valued — unknown does not grant membership, and a primitive where invariant is the dual where only a definite false denies a write — whereas under CWA unknown collapses to false in both directions. The three-valued refinement field-access discipline is a separate mechanism: a refinement predicate reading a field with no recorded value evaluates to unknown rather than failing — the K3 fail-closed boundary of Refinement, orthogonal to the concept-level world assumption. A missing field makes a predicate unknown; the world assumption then decides whether that unknown collapses to false (CWA) or is preserved (OWA).
Required-field completeness under CWA. Under the closed-world default a required field that is unasserted is a schema violation, not an unknown. OE1014 RequiredFieldUnasserted fires for the in-body construction case — a mutate body that classifies an individual with insert iof and populates its fields in the same body — at body end / commit time (see mutate). The construction-time gate for brace construction (insert T { … } missing a required field, OE0207) is unconditional. Under an #[world(open)] concept an unasserted required field is unknown rather than a violation.
Cross-world conservativity (mechanized). A CWA-true result lifts safely into an OWA consumer — positive evidence stays positive when more information may yet arrive. The base transfer theorem is mechanized at TypeSystem/Soundness/CwaOwa.lean (cwa_owa_transfer): a refinement membership established under CWA remains valid when re-evaluated under OWA, and cwa_isCwa_preserved_under_info_increase extends this to a more-informative consistent (K3) state. The reverse is provably not sound (owa_to_cwa_not_sound): a CWA conclusion that holds only via the closed-world collapse of an unknown to false does not transfer. The module is deliberately parameterized over the WorldAssumption — the CWA default is a surface/engine policy, not a substrate theorem, so the mechanization arbitrates the relationship between the two regimes without privileging either. The bitemporal lift of the transfer is in Locality/Temporal.lean.
Module-extraction interaction. Cross-module imports preserve CWA conclusions about the imported signature via ⊥-local module extraction; the Σ-scoped conservativity theorems are in Module extraction (tree-shaking) (Locality/ScopedConservativity.lean, Locality/DomainConservative.lean).
Per-(standpoint, concept), not per-(concept, time). A world assumption is declared per-(standpoint, concept), never per-(concept, time). A modeler needing different closure disciplines across valid-time would declare two or more standpoints with VT-scoped activation conditions and compose them via standpoint federation (Standpoints and modal operators); the substrate WA-map signature has no time argument. Attempting a per-concept closure with an explicit VT scope without standpoint decomposition is refused with OE0810 WorldAssumptionTemporalIndex.
Reasoning
A derive program is a set of rules (derive); its meaning is
the set of tuples those rules derive. This chapter fixes that meaning: the least-fixpoint model of
the positive program, the stratification that orders negation, the well-founded semantics that
handles recursion through negation, and the defeasibility layer that compiles overrides onto all of
it. The surface — what a derive rule looks like and what each clause is allowed to say — is in
derive; this chapter is what the engine computes from it.
Least-fixpoint model
A positive derive program (no negation, no aggregation) denotes a single model: the least
fixpoint of its immediate-consequence operator. Start from the ground facts, apply every rule
once to derive new tuples, repeat until nothing new appears. Over a finite domain this terminates,
and the result is unique — independent of the order rules fire. Multiple derive clauses with
matching head name and arity union into one relation node, so a seed tuple and a recursive clause
over the same head share a fixpoint (seeds ∪ derived-closure).
Aggregation extends the operator without breaking uniqueness, provided each aggregate reads a strictly lower stratum than its head writes (the stratification below). The aggregate fold then sees a converged input relation before it runs, so the fixpoint over the layered program is still unique.
Stratified negation
Negation breaks the monotonicity the least fixpoint relies on: a tuple that adds to a positive
relation can remove a tuple that a not atom guarded. Stratification restores order. The
rules are partitioned along their axis dependency graph — which axes a rule’s body reads against
which axis its head writes — and each layer is evaluated to fixpoint before the layer above it
reads its negation. A rule’s body negation must read a relation already converged in a lower
stratum.
The graph must be acyclic for this layering to exist. The Lean substrate proves (Theorem 2) that a cycle in the axis dependency graph admits a Cat1/Cat2 rule pair whose stratified fixpoint is order-dependent — there is no canonical layer assignment, so the program has no single stratified model. Cross-stratum, acyclic negation is ordinary stratified negation and is evaluated by this layering.
A negation cycle that no stratification can layer — p :- not q, q :- not p — is not
rejected. It is dispatched to the well-founded semantics below. (The diagnostic that once rejected
such a cycle, OE1309, no longer fires from stratification; it survives only as the dispatch seam.)
Well-founded semantics
Negation reads against the well-founded model, computed by the Van Gelder–Ross–Schlipf 1991 alternating fixpoint. WFS is the default because the well-founded model always exists and is unique — every program has exactly one, with no choice for the engine to make. The headline consequence: recursion through negation is accepted. A negation cycle that strict stratification cannot layer is evaluated by the alternating fixpoint rather than refused.
The alternating fixpoint is three-valued: every atom is true, false, or undefined. The
query surface is two-valued. The engine materializes only the definitely-true extent — a
paradoxical atom (one the alternating fixpoint cannot pin to true or false) resolves to undefined
and simply does not fire. For conditional obligations this is the sound projection: an obligation
whose trigger is genuinely undecided neither fires nor is denied; it is omitted.
#[brave] / stable-model semantics — multiple two-valued models, with credulous and skeptical
readings — is out of scope (Out of scope). WFS is
the single evaluation discipline.
Defeasible reasoning
Real ontologies have exceptions and overrides; classical Datalog does not. Argon’s defeasibility (RFD 0028) makes them first-class without letting any rule lie about what it derives. The design has three commitments:
- Honest heads. A rule derives exactly what its head says. An unmarked
deriverule is strict — classical Datalog, exactly asderive; its conclusions cannot be overridden. - The attack is a directive, not grammar. Whether one rule displaces another is a statement about rules, carried in the directive plane above the rule, never a clause inside its body.
- Strategy is a compilation scheme. The meaning of a defeasible program is the meaning of its compilation onto the core stratified/WFS semantics. No separate reasoner runs; the engine stays strategy-blind.
The directive vocabulary
| Directive | On | Meaning |
|---|---|---|
#[default] | a derive rule | this clause is overridable — it holds unless an applicable attacker blocks it (the Rust default fn reading) |
#[defeats(target(args))] | a derive rule | when this rule’s body fires, it blocks the targeted conclusion for the bound tuples |
#[label(name)] | a derive rule | gives the clause an identity, referenced as head.label |
The canonical example — adults can vote by default, felons are disenfranchised, special-class members vote regardless — reads true at every line:
// strict = unmarked: special-class members vote, period (unattackable)
pub derive can_vote(p) :- SpecialClass(p);
// the overridable default
#[default]
#[label(adult)]
pub derive can_vote(p) :- Adult(p);
// the exception: an honest head, and the attack as a directive
#[defeats(can_vote(p))]
pub derive disenfranchised(p) :- Felon(p);
No rule spells the head it denies. The exception lives under its own name (disenfranchised);
the attack rides the directive plane. The pure “block without asserting” defeater (Governatori’s
⇝) is the degenerate case — a #[defeats(…)] rule whose head no one reads.
Targeting
A #[defeats] target is resolution-checked at elaboration (goto-def-able); an unresolvable
target refuses loudly. Three targeting forms ship:
- Head-level —
#[defeats(can_vote(p))]: attacks every#[default]clause of that head. - Clause-level —
#[defeats(can_vote.adult(p))]: attacks exactly the clause labeledadult. This is lex specialis — the specific rule defeating the general clause’s label, an explicit cross-package-stable edge rather than a pair of magic priority integers. - Trait-qualified —
#[defeats(Vote::can_vote(p) @ A)]: attacks a trait member’s clause at a given impl target, using the qualified catalog naming (Rule-atom grammar). Exceptions compose across modules: a regulation package can defeat a clause it does not own.
Arguments resolve against the decorated rule’s variables. #[defeats(can_vote(p))] on
disenfranchised(p) blocks can_vote exactly for the p the attacker derives — per-tuple
blocking, not head-wide suppression. A #[defeats] argument that binds in neither the head nor
the body of the rule is a loud error (OE0721), never a fresh variable.
Discipline
- Strict conclusions are unattackable (
OE0717). A#[defeats]target resolving to a head or clause not marked#[default]refuses: adding rules to a classical program can only add conclusions. - Defeat-graph cycles are refused (
OE0718). The graph over resolved rule identities is acyclic by build-time check; cyclic attack structures are where the well-behaved compilation stories diverge, and Argon refuses rather than picking one silently. - Defeated defeaters are legal — a
#[defeats]rule may itself be#[default]and be attacked in turn (the exception to the exception), as long as the chain bottoms out. - Team defeat, ambiguity blocking. A tuple is in the head’s extent iff some clause not attacked on that tuple derives it — an unbeaten teammate keeps the conclusion. A blocked tuple is simply absent from the extent; it does not propagate a third truth value downstream.
- Duplicate labels per head refuse (
OE0719); labels are per-head identities.
Strategy #1: Governatori with explicit superiority
The strategy is Governatori-style defeasible logic with explicit superiority and ambiguity blocking, specified as its Maher-2021 three-stratum compilation onto the core stratified/WFS semantics:
- Support — run every clause (strict + default, plus the attacker rules’ own honest heads) to fixpoint over the materialized base, and attribute each clause’s contribution over that converged catalog — a recursive clause sees its own prior tuples, so transitive closure does not under-derive.
- Blocking, from surviving attackers — project each attacker’s surviving extent (resolved in defeat-graph topological order, which exists because the graph is acyclic) through its edge argument binding to the blocked target tuples (per-tuple). A defeated defeater contributes nothing — it no longer blocks on the tuples where it was itself defeated.
- Team-defeat fold — strict clauses contribute unconditionally; a default clause contributes the tuples no surviving edge blocks.
The strategy id is recorded in the .oxbin so an artifact is honest about which compilation gave
it its meaning. Future strategies (default logic, courteous LP, argumentation, ASP preferences)
arrive as use-imported macro-vocabulary packages selected per module; the engine never changes.
Proof tags
Every derived fact carries one of four tags (Rule-atom grammar),
surfaced through the provenance channel (ox derive --explain):
+Δdefinitely provable — supported by a strict (unattackable) clause.−Δdefinitely refuted.+∂defeasibly provable — a surviving#[default]clause supports it after defeat resolution.−∂defeasibly refuted — every supporting clause was attacked.
$ ox derive examples/legal_norms_can_vote can_vote --explain
+Δ (dave) // strict special-class clause
+∂ (alice) // surviving adult default
The Lean substrate proves the defeat algebra over each clause’s converged contribution: the
team-defeat fold realizes the declarative per-tuple warranted set
(Argon.Reasoning.Defeasibility.Transform.compiled_extent_eq_warranted), strict conclusions are
unattackable (strict_clause_unattackable), ambiguity blocking holds
(all_supporting_clauses_attacked_absent), and — modelling the blocking sets as derived from
the attackers’ surviving extents rather than as opaque inputs — a defeated defeater no longer
blocks (defeated_defeater_does_not_block, block_mono_in_survivors, pardoned_target_survives).
The safe interaction with occurrence typing carries over — only narrowings established by
strict (non-#[default]) rules are preserved under defeasible attack
(Argon.TypeSystem.Soundness.Defeasibility).
Migration from the strength triple
The pre-RFD-0028 surface — #[strict] / #[defeasible] / #[defeater], #[priority(N)], and
pub priority blocks — is removed, and refuses loudly (OE0722) with no silent aliasing.
The map:
#[defeasible]→#[default].#[defeater](which spelled the head it denied) → an ordinary rule under its own honest head carrying#[defeats(target(args))]; the targeted clause must be#[default].#[strict]→ delete it; unmarked rules are already strict.#[priority(N)]/pub priority→ an explicit#[defeats]edge. Lex specialis is the specific rule defeating the general clause’s#[label]. Derived superiority (lex posterior over enactment dates) belongs to a future strategy vocabulary that derives edges.
Standpoints and modal operators
Standpoint declaration and federation
standpoint-decl ::= attribute* 'pub'? 'standpoint' Ident ('<:' TypeExpr (',' …)*)?
'{' mod-item* '}'
pub standpoint USFederalTax {
pub type TaxableIncome <: Money;
pub derive owes_tax(p: Person) :- p.income: TaxableIncome, p.income > 0;
}
pub standpoint California <: USFederalTax {
pub derive state_owes_tax(p: Person) :- /* … */;
}
pub standpoint ContestedClaims { /* … */ }
A standpoint is a namespace-bearing module: items declared inside are accessible as StandpointName::ItemName (e.g., USFederalTax::owes_tax). The <: relation between standpoints declares lattice membership (federation ancestor for across [...] queries), not namespace inheritance — a child standpoint does not automatically see the parent’s items; explicit use is required to bring items into scope. Cross-standpoint federation via the across [...] annotation on queries; results may carry Truth4 values.
Nested standpoint declarations are not admitted — lattice edges come from <:, not from textual containment. Submodules (mod) and impl blocks inside a standpoint are permitted.
Federation conflict policy is chosen per query, not per standpoint: a federated across [...] query is paraconsistent by default — disagreement surfaces as Both (see truth values) — and may request strict projection. Conflict policy is a per-query projection, not a standpoint-level declaration.
Visibility composition. Facts are scoped by where they are asserted. A fact/not_fact declared inside a standpoint s { … } block is local to s; one declared at file/module scope, outside any standpoint block, sits in the DEFAULT (unscoped) layer. The two compose by the sheaf reading (Sheaf-theoretic semantics for standpoint federation):
- The DEFAULT layer restricts into every view. An unscoped assertion is visible to an unfederated query, to every standpoint-scoped query, and to every standpoint in an
across [...]federation — it is the broadest perspective, and the sheaf restriction maps carry it into every stalk. - A standpoint-scoped fact is invisible outside its standpoint. It does not appear in an unfederated (unscoped) query, nor in a query scoped to a different standpoint. Reading a scoped fact requires explicitly selecting its standpoint — via
across [...],box/diamond(Modal operators), or a bridge rule (Bridge rules).
So an unfederated query (pub query q() -> P;) reads exactly the DEFAULT layer; a single-standpoint federation (across [s]) reads the DEFAULT layer unioned with s’s own facts; a multi-standpoint federation info-joins those per-standpoint views (Sheaf-theoretic semantics for standpoint federation). Derive rules, checks, and the mutation delta-guard all evaluate over the DEFAULT-layer (base) view — a scoped fact never silently becomes global truth. A federated query that names a standpoint with no declaration is refused (OE1103 UndeclaredStandpointInAcross) rather than contributing an empty extent that would mask the typo. Mechanized at spec/lean/Argon/Standpoint/Visibility.lean: view_of_base_eq_default_layer (base = DEFAULT layer), scoped_view_eq_default_union_own (scoped = DEFAULT ∪ own standpoint), default_visible_everywhere, scoped_invisible_to_base, and scoped_invisible_to_other.
Modal operators
box(P) and diamond(P) are rule atoms (Rule-atom grammar) admitted at tier:modal. They are interpreted over a Kripke frame; the frame is determined by the surrounding context.
Frames. Argon recognizes two intrinsic frames:
- Standpoint frame. Worlds are standpoints; the accessibility relation is the reflexive-transitive closure of
<:over the standpoints in scope (extended by the explicit set of any enclosingacross [...]clause).box(P)at standpointsholds iffPholds at everys'reachable fromsalong the frame. - Classification frame. Worlds are configurations in which an individual exists (store states reachable by mutations). A type introduced by a
fixedmetatype (metatype, RFD 0027 D6) carries forward rigidity: once an individual is a member, themutatemutation gate preserves that membership forward along accessibility (Argon.Runtime.ModifierGates.runMutation_fixed_iof_constant). This is one-directional, not two-way constancy — a current non-member can still later be constructed into afixedtype, sofixedpins membership going forward but says nothing about future gains. A type introduced by an unmodified (dynamic) metatype carries no such guarantee. This is the standard Guizzardi/UFO modal characterization of rigidity, realized operationally by the ontology-neutralfixedbit rather than by reading any vocabulary’srigidityaxis.
diamond(P) is dual: it holds when P holds in at least one accessible world.
Static discharge. When P is a positive type-classification atom over a target introduced by a fixed metatype, forward rigidity preserves membership across accessible worlds and the elaborator can discharge the modal at compile time without consulting a reasoner (Argon.Reasoning.StaticDischarge.box_fixed_discharge). The discharge is polarity-asymmetric: forward rigidity grips box(x : T) but gives no grip on the negation box(¬(x : T)), since a current non-member may still be constructed into the type later (Argon.Reasoning.StaticDischarge.isRigidIn_does_not_discharge_box_neg).
| Atom | Target | Reduces to |
|---|---|---|
box(x : T) | fixed-introduced | x : T |
diamond(x : T) | fixed-introduced | x : T |
box(¬(x : T)) | fixed-introduced | not discharged — needs declared disjointness |
box(x : T) | dynamic (the default) | modal reasoner |
diamond(x : T) | dynamic (the default) | exists w: World, iof(x, T, w) |
box(P) | non-classifier P | modal reasoner |
Atoms that survive static discharge are routed to a modal reasoner over the std::kripke carrier; the choice of backend (tableau, resolution, bounded model-checking) is implementation-defined.
std::kripke. Exported from the stdlib for explicit modal modeling — e.g., writing forall w: World where accessible(current, w), P@w inside unsafe logic { }. The carrier is minimal:
// in std::kripke
pub struct World;
pub struct Entity;
pub fn iof(e: Entity, t: Top, w: World) -> Bool; // instance-of-in-world
pub fn accessible(w1: World, w2: World) -> Bool; // frame accessibility
pub fn current() -> World; // the current world
The surface operators box(P) and diamond(P) are syntactic forms — not function calls — and desugar to FOL over World:
| Surface | Desugared |
|---|---|
box(P) | forall w: World where accessible(current(), w), P@w |
diamond(P) | exists w: World where accessible(current(), w), P@w |
Where P@w denotes the rule atom P evaluated at world w (i.e., relativized to w’s extension). Modelers using only the surface operators need not import std::kripke directly; the desugaring threads through the elaborator. There are no Box / Diamond runtime functions — modal operators reduce to quantification over worlds.
Federation interaction. Inside a query annotated across [s₁, s₂, …], the standpoint frame is extended to the named set; box(P) reads “P holds in every listed standpoint” and may return Both, Can, or Not per truth values when the standpoints disagree. Classification-frame box/diamond atoms compose with federation by being evaluated independently at each listed standpoint, then info-joined via the FDE lattice.
Lowering. box(P) and diamond(P) lower to AtomIR::Modal and classify at tier:modal. Cross-nesting a modal with a temporal operator is refused by the tier classifier. examples/modal_box_diamond.ar exercises both operators end-to-end.
Bridge rules
Bridge rules thread information explicitly between standpoints. Where across [...] composes standpoints lattice-wise (FDE info-join over <:), a bridge rule is a directional, named, typed inference that moves a conclusion from one standpoint to another, optionally applying a domain mapping.
Status. pub bridge is refused at ox check / ox build with OE1102 (audit doc-04 / issue #151). Bridge rules parse, lower to wire events, resolve, and pass well-formedness checks — but the federation fixpoint never fires them, so a built artifact’s bridge bodies would silently never contribute to their target standpoint. Rather than ship that inert surface, a bridge declaration is refused loudly; model the cross-standpoint inference with an explicit derive / pub fact for now. Bridge evaluation is the post-stable flagship standpoint deliverable: the parse / lower / wire / decode surface is retained intact (Module::bridges_targeting, the BridgeDecl codec) so the work resumes without a grammar or wire-format change. The semantics below is the committed design.
bridge-decl ::= attribute* 'pub'? 'bridge' Ident
'(' standpoint-ref '->' standpoint-ref ')'
'{' bridge-rule (';' bridge-rule)* ';'? '}'
bridge-rule ::= rule-body '=>' bridge-head
bridge-head ::= predicate-call // direct assertion into target
| predicate-call 'mapping' mapping-clause // with domain map
mapping-clause ::= '{' (Ident '=' expr (',' …)*)? '}' // src-var ↦ dst-expr
standpoint-ref ::= path // resolves to a standpoint
pub bridge USCitizens(US::Tax -> International::Legal) {
Person(p), p.citizenship == "US"
=> LegalEntity(q) mapping { q.jurisdiction = "US", q.person_ref = p };
Person(p), p.passport != null
=> Verified(p);
}
The -> between standpoint references in the bridge head encodes the direction; existing -> operator (function return arrow) is reused with no new lexer token.
Directionality (key property). A bridge from s₁ to s₂ does not imply a bridge from s₂ to s₁. The asymmetry is what makes bridges different from equality / equivalence axioms; without it, bridge composition would collapse standpoints and destroy their contextual scoping. This is the standard discipline across DFOL (Ghidini-Serafini), DDL / C-OWL (Borgida-Serafini-Bouquet), and MCS (Brewka-Eiter) — see the vault notes on bridge-rule semantics for the comparative landscape.
Body and head. The rule body uses the standard rule-atom grammar (Rule-atom grammar) evaluated at the source standpoint. The head is a predicate call evaluated at the target standpoint. If source and target have different domains (e.g., a tax-jurisdiction Person vs. an international-law LegalEntity), an explicit mapping { … } clause specifies how source variables map to target-domain values; without a mapping clause, the bridge is identity (source variable = target argument).
Negation-as-failure in bridge bodies. Bridge bodies admit NAF (not atom) over the source standpoint’s predicates — matching the MCS form. The bridge graph (nodes = standpoints; edges = bridges) must be stratified for the federation fixpoint to be unique; cycles in the bridge graph emit OE1101 BridgeCycle.
Composition with across [...]. A query annotated across [s₁, s₂] composes lattice federation (via <:) and any bridges between s₁ and s₂ in scope. Both contributions are info-joined via the FDE lattice; conflicts surface as Both per truth values.
Mechanization. The standpoint-frame sheaf semantics (Sheaf-theoretic semantics for standpoint federation) treats bridges as restriction maps on the perspectival sheaf — see that section. Diagnostic codes for malformed bridges live in the OE11xx range (Standpoints / perspectival config).
Bridges under temporal extent (Temporal substrate). When bridges cross modules with bitemporal extent, the substrate admits only temporal-uniform bridges at tier: recursive: a bridge rule consults facts only at the same valid-time at which it fires. Bridges that consult facts at different times (bridge m s t reading s _ t' with $t’ \ne t$) require tier: fol and an explicit temporal-bridge-monotonicity axiom in the module’s prelude. The sheaf-equivalence theorems of Sheaf-theoretic semantics for standpoint federation lift pointwise under temporal-uniform bridges with no new axioms; non-uniform bridges require a new axiom and are out of the conservativity envelope. Diagnostic: OE0720 NonUniformBridgeRefused. Mechanized at spec/lean/Argon/Locality/Temporal.lean (sheaf_equivalence_bitemporal_uniform).
Sheaf-theoretic semantics for standpoint federation
The standpoint lattice carries a sheaf-theoretic semantics — the formal grounding of why across [...] federation, bridge rules (Bridge rules), and box/diamond modal operators all compose coherently.
The sheaf. Treat the standpoint lattice as a base for a Grothendieck topology: open sets are downward-closed subsets of the <: lattice. The perspectival sheaf 𝓕 assigns to each open set U a stalk 𝓕(U) — the local knowledge (set of derivable facts, three-valued atoms, evidence accumulations) available to the standpoints in U. Restriction maps 𝓕(U) → 𝓕(V) for V ⊆ U encode how broader perspectives specialize to narrower ones; explicit bridge rules (Bridge rules) lift to additional restriction maps wherever the bridge graph reaches.
| Modal / standpoint construct | Sheaf-theoretic reading |
|---|---|
Standpoint s | Open set in the lattice topology |
box_s(P) | Section of 𝓕 over s’s open set (P holds across the perspective) |
diamond_s(P) | Local non-zero stalk witness (P holds somewhere in s’s open) |
<: between standpoints | Open-set inclusion |
| Bridge rule (Bridge rules) | Restriction map 𝓕(U) → 𝓕(V) along the bridge direction |
Consistent federation (across [s₁, s₂] returns Is(v)) | Global section over U(s₁) ∪ U(s₂) |
Irreducible disagreement (Both(a, b)) | Non-trivial first cohomology H¹(𝓕) ≠ 0 |
Substrate theorems. The correspondence is proved over a finite model at spec/lean/Argon/Locality/SheafEquivalence.lean — no axioms, no sorry: bottom_up_is_equilibrium, equilibrium_is_global_section, and equilibrium_is_minimal_section. Stated informally: the bottom-up evaluation of the federated standpoint system produces an MCS-style equilibrium, and that equilibrium is a minimal global section of the perspectival sheaf. The full categorical construction — a genuine Grothendieck topology with general restriction maps — is open research (it would unify two independent formalizations of perspectival reasoning; see the vault note “Topology of Understanding”). The finite-model theorems are the substrate commitment.
Consequence (H¹ interpretation). When across [s₁, s₂] yields Both(a, b) (the FDE conflict outcome), the first cohomology H¹(𝓕) carries a non-trivial generator. The two witnesses a, b are cohomologically distinct — they witness an irreducible perspectival disagreement that no global section can unify. This is the formal content behind the #[consistency(paraconsistent)] policy: the substrate makes the disagreement explicit rather than collapsing it.
Consequence (sheaf cohomology as diagnostics). Future tooling may surface H¹(𝓕) generators as a diagnostic class — “your federation disagreement is reducible (cohomologous to zero, fixable by a bridge rule) or irreducible (a non-trivial cohomology generator, requiring paraconsistent acceptance).” This is research-grade; the substrate axioms are the commitment.
Lift under bitemporal extent. When the standpoint federation runs over modules with bitemporal extent (Temporal substrate), the three sheaf-equivalence theorems lift pointwise without new axioms — provided bridges are temporal-uniform (Bridge rules). The bitemporal BeliefAssignment, BridgeEval, and LocalFixpoint become time-indexed functions; at each time slice, the canonical theorem applies. Mechanized at spec/lean/Argon/Locality/Temporal.lean (bottom_up_is_equilibrium_bitemporal, equilibrium_is_global_section_bitemporal, equilibrium_is_minimal_section_bitemporal, bundled as sheaf_equivalence_bitemporal_uniform). Non-uniform bridges (consulting facts at different VTs) require a new temporal-bridge-monotonicity axiom and are out of the conservativity envelope.
Decidability
Tier ladder
| Tier | Adds | Cost |
|---|---|---|
structural | subsumption (<:), disjointness, role hierarchies, partition | polytime |
closure | transitive closure, role composition, functional/inverse, reflexive/irreflexive | polytime |
expressive | qualified cardinalities, full negation, class expressions | decidable, EXP worst case |
recursive | Datalog with negation; recursion-through-negation evaluated under WFS (default) | decidable |
fol | full FOL via unsafe logic { } | semi-decidable |
modal | modal/standpoint over std::kripke | modal+FOL |
metaorder | unbounded multi-level / higher-order instantiation (Higher-order modeling, theories ship in stdlib) | bounded decidable; unbounded not |
Default module tier: structural.
Temporal sub-tier (orthogonal axis). Every program has a tier pair (main, temporal). The temporal sub-tier ladder:
| Sub-tier | Admits | Data complexity |
|---|---|---|
none | no temporal operators | n/a |
snapshot | bare iof(x, T) with bitemporal storage; no temporal operators in rules | n/a |
acyclic-stratified | MTL-acyclic stratified DatalogMTL¬ on ℤ | P |
stratified | full stratified DatalogMTL¬ on ℤ | PSPACE |
forward-propagating | forward-propagating stratified DatalogMTL¬ on ℤ | PSPACE (no past-future mixing) |
unstratified | WFS through unstratified MTL recursion | OPEN; admitted only at tier: fol via unsafe logic |
The classifier accepts a program if both axes are tractable and the program respects additive composition between modal (box/diamond) and metric temporal operators — no nesting of one family inside the other. Cross-mixed nesting box(box_minus[a,b](C)) is refused at tier: recursive (OE0712 ModalTemporalCrossNestRefused) and routed to tier: fol. The grounding is Demri-Wałęga 2024 (Standpoint LTL): satisfiability is EXPSPACE-complete unrestricted, with a PSPACE fragment under interplay restriction that Argon’s rigidity-of-standpoints commitment satisfies.
Annotation:
#[dec(tier: recursive, temporal: stratified)]
mod my_module { … }
If only one axis is annotated, the other is inferred from the module’s content.
Mechanized at spec/lean/Argon/Decidability/Temporal.lean (TemporalSubTier lattice + additiveComposition predicate + admitsAt_admits soundness lemma).
Annotations
#[dec(tier:<name>)]
#[budget(heartbeats: Nat)]
#[scope(max: Nat)]
#[brave] // stable models (reserved)
#[coinductive] // greatest fixpoint
#[constructive] // required at tier 5+ for non-ground NAF
#[shape(<recognized-shape>)]
#[lattice(K3 | FDE | Boolean)]
#[consistency(strict | paraconsistent)] // on standpoint
#[world(open | closed)] // on concept (World assumptions (CWA / OWA); live — marks the concept's world, default CWA)
#[default] // on derive — overridable clause (Defeasibility, RFD 0028)
#[defeats(target(args))] // on derive — declares an attack (Defeasibility, RFD 0028)
#[label(name)] // on derive — clause identity, head.label (Defeasibility)
#[intrinsic] // on iof-body field
#[unproven] / #[assumed] // on test
#[language_interface] // on Lean-spec-bound items
Theory-specific annotations (e.g., #[order(N)] from the mlt package, #[potency(N)] from a potency package) are library macros in unprivileged theory packages, not language-level attributes — see Higher-order modeling and Stdlib (selected).
Reserved attribute names. The compiler-known attribute names listed above are reserved. A user pub macro <name> { … } or #[procmacro] pub fn <name>(…) declaration whose name collides with the reserved set is rejected at the declaration site with OE0708 ReservedAttributeName. The full reserved set:
brave, budget, coinductive, consistency, constructive, dec, default,
defeasible, defeater, defeats, derive, intrinsic, inverse, label,
language_interface, lattice, managed, no_implicit_prelude, priority,
procmacro, scope, shape, unproven, assumed, world
(defeasible, defeater, priority are removed surfaces — they stay reserved so a typo refuses with a migration hint, RFD 0028 D9 — and default, defeats, label are the replacements.)
This list is fixed; adding to it requires an edition bump (the edition = N field in ox.toml). Theory-specific attribute names (categorizes, partitions, subordinate_to, power_type_of, order, complete, potency, @n, …) live in their own unprivileged theory package (mlt, potency, …) as #[procmacro] / pub macro declarations, not as language-reserved attributes — see Higher-order modeling and Stdlib (selected).
unsafe logic
The only escape to full FOL:
unsafe logic {
#[scope(max: 10)]
forall p: Person where exists q: Person where ParentOf(p, q)
}
The body admits the rule-atom grammar of Rule-atom grammar; FOL binding quantifiers (forall x: T where … and exists x: T where …) are accepted without tier-cap rejection. No new connectives or syntax are introduced — only the elaborator’s tier classifier differs. The tier classifier statically routes the block to a backend (native / SMT / ASP / FOL prover) and emits a per-block verdict: TERMINATES, TERMINATES UNDER BOUND (requires #[scope] / #[budget]), or REJECTED.
Higher-order modeling
Multi-level / higher-order theories live in ordinary first-party packages with zero compiler privilege — never in the language core and never in std. The substrate provides the primitives — iof (Reflection), <: (Vocabulary concepts and the generic type metatype), the meta-calculus (Meta-calculus atom), well-founded instantiation, stratified aggregates over the iof DAG (derive) — and theories layer on top. The substrate is deliberately neutral about which higher-order theory a model commits to, so none of them belongs in std (RFD 0043).
The mlt package’s ability to treat iof and specializes as first-class predicates — passing, counting over, and quantifying across type-values — rests on the substrate reflective sort TypeRef (Reflection), whose values are references to declared types; this realizes RP-003 GAP-1. The substrate TypeRef sort is the neutral handle layer and is distinct from any library construct built atop it: it is not an MLT* orderless Type term (whose powertype/order semantics live in that package) nor a vocabulary’s own Type_ concept (an ordinary declared type). TypeRef carries no order, potency, or categorization — those remain the theory packages’ concern.
Higher-order theories ship as unprivileged packages at tier:metaorder (Tier ladder), e.g.:
| Package | Theory | Surface keywords |
|---|---|---|
mlt | Multi-Level Theory (Carvalho-Almeida-Fonseca-Guizzardi) | categorizes, partitions, subordinate_to, power_type_of, order |
mlt::star | MLT* orderless extensions | Type, orderless sortality |
potency | Deep Instantiation / Potency (Atkinson-Kühne, MetaDepth) | potency, clabject, @n annotations |
Each package provides:
- its metaxes (e.g., the
mltpackage declarespub metaxis order for metatype = Nat where { _ <= 16 }), - its metarels (e.g., the
mltpackage declarespub metarel categorizes(higher: type, base: type)), - the decorator forms as library
#[procmacro]s (e.g.,#[categorizes(Animal)]re-emits the decorated declaration and emits a re-checkedMetaPropertyon thecategorizesmetarel;#[order(2)]does the same on theordermetaxis), - derived level functions over the iof graph,
- check rules enforcing the theory’s axioms (e.g., the
mltpackage enforces Carvalho-Almeida S1–S5), - the theory’s constraints as its own rules/checks over the neutral substrate — never compiler-reserved diagnostic codes; the substrate ships no theory-namespaced codes.
Intrinsic-by-default for higher-order types. When the mlt package’s cross-level decorators (#[categorizes], #[partitions], #[subordinate_to], #[power_type_of]) are applied to a type declaration, the type’s fields default to #[intrinsic] — every iof-instance must supply a value (field = value at kind level) or carry a from-navigation source (struct and enum — language built-ins (data declarations)). A field with neither emits OE1908 IntrinsicPropertyMissing against every iof-instance. Modelers opt a field out by declaring it T? (optional, yields None when absent). The field-default opt-out (field: Type = expr) refuses with OE0237 FieldDefaultUnsupported (see struct and enum — language built-ins (data declarations)). Parallel decorators in a potency, ml2, or user theory package may declare their own intrinsic-default rules.
A modeler picks a theory by listing its package in ox.toml and importing it: use mlt::*; brings MLT’s vocabulary into scope; a third-party vocabulary package (e.g. an externally-authored UFO package) may re-export mlt::* plus its own metatypes (kind, role, phase, …) on top.
The std::level::Stratified trait gives consumers a theory-agnostic level handle:
// in std::level
pub trait Stratified {
type Level;
fn level(self) -> Self::Level;
}
Each theory implements Stratified for its entities; consumers depend on the trait, not the theory.
See the stdlib reference (Stdlib (selected)) for full package documentation. Decidability tier metaorder (Tier ladder) admits unbounded multi-level / higher-order instantiation; theory packages may surface their own bounded variants (e.g., a package may cap MLT order to lower the module to a polynomial tier).
Prior versions of this specification embedded MLT directly in Higher-order modeling with built-in decorators and OE19xx diagnostic codes. That commitment is superseded; see
spec/research/RP-003-mlt-as-library.mdfor the feasibility study and migration rationale.
Build pipeline and .oxbin
An Argon workspace compiles to one content-addressed file: .oxbin. This chapter is the build contract: what ox build accepts and rejects (ox build is fail-closed), the pipeline that produces the artifact (Pipeline), how a composition gets its cryptographic identity (Composition signature), the artifact’s section model (Section model), versioning (Versioning — four orthogonal axes), content-addressing (Content-addressing), and load-time validation (Load-time validation — two layers). The byte-level wire format is documented in the oxc-oxbin crate (compiler/crates/oxc-oxbin/FORMAT.md); the axiom-ADT body variants the events section carries are catalogued in Axiom-ADT variant catalog.
ox build is fail-closed
A built artifact evaluates every rule it contains, or it does not exist. ox build runs three gates in order; any gate that fails aborts the build and writes no .oxbin. A wrong artifact is worse than no artifact.
-
Parse gate. Elaboration is error-tolerant — it recovers from malformed input by dropping the broken construct. Left unchecked, a parse error would silently produce an
.oxbinmissing whatever the parser choked on. Soox buildfirst collects every error-severity parse diagnostic; if any exist, it prints them and exits 1 before resolution runs (oxc-driver/src/lib.rs:759). -
Resolution and type gate. It then runs the full resolve/typecheck pass (
collect_check_diagnostics). Any error-severity diagnostic — an unresolved predicate, a type error — aborts the build with no artifact. -
Un-evaluable-rule oracle gate. The lowering pipeline admits some rule shapes the runtime’s rule compiler then refuses. Such a rule is silently dropped from evaluation, so an artifact containing one returns wrong (under-derived) query answers with no runtime error. To prevent that,
ox buildloads the elaborated artifact and re-runs the runtime’s owncompile_ruleas an oracle over every rule (Module::uncompilable_rules). If any rule will not evaluate, the build fails loudly and writes nothing.ox checkenforces the same oracle through the driver’s internalvalidate_runtime_evaluable_rulespath, so editor/CI validation cannot green-light a package thatox buildlater rejects. This intentionally makesox checkdo the extra work needed for artifact-parity validation: package elaboration, stdlib loading, comptime lifting, event encoding, andModule::load. Refused shapes:Shape Diagnostic Unsafe rule — a head / negated / comparison / compute variable not bound by a positive body atom (range-restriction) OE1303Nested aggregate ( count { count { … } > N })OE1311Collection/string aggregate kind ( collect/set_collect/string_join/percentile); the folding kindscount/sum/min/max/avg/count_distinctevaluateOE1312notwrapping an aggregate subqueryOE1313Empty aggregate body ( count { })OE1314Unsupported quantifier shape ( existsbinder,forall(path, T), under-specifiedforall)OE1315Rule bodies the semi-naive executor does not compile — a type-test atom or a meta-equality (
==onmeta(x)) — are recorded the same way and surface as the same load-time refusal (oxc-runtime/src/lib.rs:299).The oracle reports executor-capability failures. A hard
Module::loadfailure surfaces through normal artifact load/build diagnostics.The fix is always to rephrase the rule into a supported shape — typically by exposing the offending subterm through an intermediate
pub deriverule. The supported universalforall v: T where Body, Head(the lastwhereatom is the consequent, the rest the domain) does evaluate; it lowers to a count-equality.
Pipeline
ox build <dir> searches upward from <dir> for the nearest ox.toml and treats that directory as the package (oxc-workspace::find_nearest_package_dir), the same cargo-style discovery ox check and ox lsp use. Module resolution then handles real nested package layouts — pkg::, super::, self:: path roots and pub use re-exports (see Modules and packages; not repeated here).
Output location. Build products never sit next to sources — cargo-style, the artifact is written under a target/ directory: <root>/target/<name>.oxbin, where <root> is the package directory (the one holding ox.toml) and <name> is the resolved entry’s file stem (so a package whose entry is root.ar yields target/root.oxbin). For a standalone .ar file with no enclosing manifest, target/ is created beside the file. target/ is created on demand and is git-ignored. -o/--out overrides the path entirely. The artifact-loading commands (ox query, ox derive, ox run, ox run-scenario, and ox runtime serve --oxbin) accept a package directory or source .ar and resolve the same default target/ location, or an explicit .oxbin path.
The pipeline has a sharp boundary: oxc is the per-package compiler; ox is the workspace orchestrator. Every cross-package concern lives in ox.
per-package source
↓ oxc elaborate ← oxc owns (sync; salsa-cached)
per-package CoreIR
↓ oxc instantiate ← oxc owns; consumes wiring
per-package .oxc cache
↓ ─┐
(workspace metadata + lockfile) │ ox owns:
↓ ox build │ • compute wiring diagram
↓ │ • compose standpoint lattice
↓ │ • check tier cap
↓ ─┘ • merge .oxc → .oxbin
workspace .oxbin
↓ runtime backend (Runtime contract)
answers / docs / mutations / …
Three artifacts, three typed transitions:
- Source — the
.arsurface text plus the per-package manifest. .oxccache — one file per package: the elaborated, instantiated serialization of that package’s contribution relative to this composition. Same section model as Section model, smaller scope..oxbin— the workspace artifact: one file perox build, content-addressed, consumed by a runtime backend (Runtime contract).
oxc never sees the workspace. It elaborates a single package against a wiring diagram ox supplies; it cannot resolve cross-package references, merge standpoint lattices, check tier consistency across packages, or emit the workspace .oxbin. ox is the only command modelers invoke directly (ox build, ox run, ox query, ox check, ox test, ox doc, ox inspect, ox runtime serve, per reserved keywords); oxc instantiate / oxc emit core-ir / oxc dump are tooling-internal.
Build scope (RFD 0030). ox build produces a single, monolithic .oxbin per invocation covering the entry’s dependency closure: each [dependencies] path dependency’s modules are loaded and folded into the consumer’s workspace (exactly as the embedded stdlib is folded), elaborated into the same event stream, and embedded in the one artifact — so a dependency’s content participates in the artifact’s content hashes (Content-addressing). Cross-package references across a path dependency resolve at build time. The registry/git dependency surface (a bare version requirement, the version/git/branch/tag/rev/registry keys, ox.lock) is out of scope (Out of scope) and refuses with OE1240. The separate per-package .oxc cache and workspace-merge orchestrator below (Composition signature) describe the composition model the registry story realizes.
Composition signature
The wiring diagram is the workspace-level resolution of every cross-package symbol reference — the closed-world view of how packages compose. ox computes it from the package graph and the lockfile.
WiringDiagram :=
map from (consumer_package, ref_site)
to (target_package, target_qualified_path, target_stable_id)
The composition signature is the cryptographic identity of one composition:
composition_signature := BLAKE3-256(
wiring_diagram_hash ∥ // BLAKE3-256 over the canonical resolved symbol table
standpoint_lattice_hash ∥ // BLAKE3-256 over canonical-form composed lattice
tier_ladder_version ∥ // u32 LE (preamble, Versioning — four orthogonal axes)
runtime_contract_version // u32 LE (preamble, Versioning — four orthogonal axes)
)
The hash is BLAKE3-256 throughout the content-addressing story (RFD 0001 Phase 2 replaced SHA-256). ox build computes wiring_diagram_hash as a BLAKE3 over the canonically-sorted (axiom-kind, content-id) of the structural axiom events — the schema-defining declarations, axioms, and rule-modes — so it is sensitive to a relation/concept rename or a rule change but stable under instance-data churn (asserted facts carry no structural weight); standpoint_lattice_hash is the same construction over the standpoint and bridge declarations.
The wiring diagram source is not preserved in .oxbin — it is a transient build-time input. Only the four sub-hashes survive (wiring_diagram_hash and standpoint_lattice_hash in global-control; the two version u32s in the preamble). ox build checks that the wiring is correct; the runtime re-verifies at load that the stored sub-hashes have not been tampered with. Two semantically equivalent compositions produce byte-equivalent .oxbin files — the central content-addressing invariant (Content-addressing).
Section model
A .oxbin is a preamble followed by a sequence of typed sections. Ten section types are defined — five mandatory, five optional:
| Section | Mandatory | Load discipline | Holds |
|---|---|---|---|
global-control | ✓ | synchronous | Section directory; the two sub-hashes; tier caps |
symbol-table | ✓ | synchronous | Every reference resolves through it |
events | ✓ | synchronous (mmap) | Source of truth for axioms, retractions, derived events |
standpoint-lattice | ✓ | synchronous | The lattice each event row’s standpoint_id indexes into |
tier-table | ✓ | synchronous | Per-rule tier-pair classification consumed by Layer 2 (Load-time validation — two layers) |
fork-lineage | optional | sync when present | Omitted by artifacts without forks (most published packages) |
projection-cache | optional | LAZY mmap | Maintained projections; runtime re-saturates if absent |
arrangement-section | optional | LAZY mmap | DBSP-shaped Z-set arrangements; forward-compat for IVM |
pathology-flags | optional | sync when present | Compile-time-detected reasoning pathologies |
doc-blocks | optional | LAZY per-item | Doc strings; the runtime works without them, ox doc requires them |
Optional and LAZY are independent: optional governs presence in the artifact; LAZY governs whether the runtime mmaps the section on first access rather than reading it in the synchronous load prefix. Section type-ids live in one uint8 namespace — 0..99 standard, 100..199 Argon-future, 200..255 user-custom (unknown custom sections are ignored on load unless MANDATORY-flagged). The section directory in global-control indexes every section; its order is canonical load order. The runtime tolerates a mandatory section that is present but empty. Byte layout of the preamble, per-section sub-headers, and per-section encodings is documented in the oxc-oxbin crate.
Versioning — four orthogonal axes
The preamble carries four independent uint32 version axes, each bumped on its own:
| Axis | Bumped when | Minor / major |
|---|---|---|
oxbin_format_version | Section model or encoding changes | Minor: additive section; major: breaking section/encoding |
core_ir_version | The CoreIR axiom-ADT variant set changes | Minor: additive variant/field; major: removal/breaking |
tier_ladder_version | The decidability ladder definition changes | Major only — tiers are semantic, not additive |
runtime_contract_version | The OxbinRuntime trait surface (Runtime contract) changes | Minor: additive method; major: breaking signature |
Robustness: producers strict, consumers liberal, per axis.
- A consumer accepts a future minor bump on any axis: load proceeds and unknown additive things degrade gracefully (an unrecognized non-
MANDATORYsection is skipped). - A consumer refuses a future major bump on any axis: load fails with
OE1202 IncompatibleVersionAxis(axis, want, got). - A consumer refuses any
MANDATORY-flagged section it does not recognize, regardless of axis:OE1203 UnknownMandatorySection(section_type).
Within one oxbin_format_version major, an artifact MAY carry two representations of the same logical content side by side (e.g. a DRedc-shaped projection-cache and a DBSP-shaped arrangement-section); each runtime picks the section it understands.
There is no kernel_api_version axis — the runtime contract is the OxbinRuntime trait (Runtime contract), versioned by runtime_contract_version. A backend’s internal schema version (e.g. the Postgres backend’s migrations, Storage layer) is not carried in the preamble; backends version their own schemas.
Content-addressing
The artifact-level hash is a Merkle root over the composition and the section directory:
artifact_hash := SHA-256(
composition_signature ∥ // Composition signature
section_directory_canonical_form // deterministic-CBOR section directory
)
Each section additionally carries a content_hash (SHA-256 over its encoded body) when CONTENT_HASHED-flagged. Two semantically equivalent compositions produce byte-equivalent .oxbin files, hence an identical artifact_hash. This holds because every section uses deterministic encoding (RFC 8949 deterministic CBOR for structural sections) plus per-section canonical-form rules (canonicalization is part of the oxc-oxbin format contract). The payoff:
- Distributed caching — re-running
ox buildon the same inputs is a cache hit onartifact_hash. - Selective invalidation — a TBox change touches only the
eventscontent hash; downstream caches invalidate at section granularity. - Content-addressed distribution — an
.oxbincan be named by itsartifact_hashin a CAS/Nix/IPFS-style registry. - Tamper detection — any byte changed after
ox buildis caught at load.
Load-time validation — two layers
The runtime contract requires two validation layers.
Layer 1 — closed boolean. Computed once global-control is parsed, before any other section body is read; constant-time per artifact:
tier_valid(.oxbin, runtime)
:= global_control.max_tier_claimed ≤ runtime.max_tier_supported
∧ global_control.max_temporal_claimed ≤ runtime.max_temporal_supported
max_tier_claimed and max_temporal_claimed reflect the highest tier pair (per Decidability and RP-004) of any rule in the artifact. Failure refuses the artifact with OE1204 TierMismatch(want, got).
Layer 2 — per-section verifier. Run after Layer 1 passes:
| Invariant | Section(s) | Diagnostic |
|---|---|---|
| Symbol resolution | references in events/rules/queries | OE1210 SymbolResolutionFailed |
| Standpoint lattice acyclicity | standpoint-lattice | OE1211 LatticeCycle |
| Provenance well-formedness | derivation columns in events | OE1212 ProvenanceNotDNF |
| Composition-signature consistency | preamble + global-control | OE1213 SignatureMismatch |
| Tier consistency | tier-table vs max_tier_claimed | OE1214 TierTableInconsistent |
| Doc-block well-formedness | doc-blocks (when present) | OE1215 DocLinkUnresolved |
Policy is per runtime kind, made explicit at OxbinRuntime::load (Runtime contract):
- Sandboxed runtimes (untrusted content) default strict — any Layer 2 failure refuses the load.
- Trusted runtimes (in-process from a trusted
ox build) default lenient — non-load-bearing failures (e.g.OE1215) warn and proceed; symbol-resolution and lattice failures are always fatal.
RP-004 hooks
The bitemporal substrate of RP-004 (Types and refinement, Rule atom — fn, derive, query, mutate, check, Decidability, Standpoints and modal operators) flows through .oxbin natively:
- The
eventssort key is(tenant_id, fork_id, standpoint_id, predicate, valid_time, tx_time)— bitemporal-native ordering, no Layer-2 rewrite for temporal queries. global-controlcarries bothmax_tier_claimedandmax_temporal_claimed; Layer 1 checks both.tier-tablerows carry a(main_tier, temporal_tier)pair per rule, mirroringDecidability/Temporal.lean’sclassifyPair. Cross-nest violations (OE0712 ModalTemporalCrossNestRefused) are rejected at compose time, before the artifact is emitted.pathology-flagsincludes an “additive-composition violation detected at compose time” flag, distinguishing provably-correct artifacts from trusted-by-compiler ones.
Runtime serving command
ox runtime serve exposes the Runtime contract serving surface around one compiled artifact. It does not compile source — a caller runs ox build first and passes the resulting .oxbin.
ox runtime serve \
--project . \
--oxbin target/main.oxbin \
--host 127.0.0.1 --port 7780 \
--storage mem
Postgres-backed serving uses the same .oxbin plus a durable ABox event log (--storage pg --database-url "$DATABASE_URL"). Watch-mode compilation, if tooling enables it, is a separate process that rewrites the artifact; the serving process hot-reloads only after the Runtime contract compatibility checks pass.
Mechanization
The wire shape, validation predicates, composition_signature, and four-axis versioning are mechanized in spec/lean/Argon/BuildArtifact/: Section.lean (section type + flags, @[language_interface]), Header.lean (preamble + version axes), Oxbin.lean (the Oxbin record + section invariants), ContentHash.lean (Merkle-of-sections), Validation.lean (Layer 1 + Layer 2 predicates), CompositionSignature.lean, and Versioning.lean (four-axis robustness). The Rust crate oxc-protocol::oxbin mirrors the @[language_interface]-tagged inductives; CI fails on drift.
Editor extensions — oxup extension
The Argon editor integration is installed and kept in sync by the toolchain manager (oxup), not by hand. oxup extension (alias ext) manages it, abstracted over editors (RFD 0032).
oxup extension install [--editor <vscode|cursor|codium|code-insiders|code-server|neovim|vim|emacs>] [--archive <path.vsix>]
oxup extension uninstall [--editor <name>]
oxup extension list
install (no --editor) auto-detects every installed VS Code-family editor (by its CLI on PATH) and installs the extension matching the active toolchain version into each. The version coupling is real: the published .vsix is stamped with the toolchain version and addressed at an immutable CDN path, https://argon.sharpe-dev.com/editors/vscode/<version>/argon-<version>.vsix. oxup resolves the active toolchain’s concrete version (from its manifest.toml), fetches that .vsix, sha256-verifies it against the published sidecar (the same fail-closed discipline as the toolchain fetch), and hands it to the editor CLI’s --install-extension. --archive <path.vsix> installs a local .vsix offline instead of fetching.
oxup init (non-minimal) and oxup update auto-install / refresh the extension after the toolchain is placed; oxup init --no-extension skips it, and update re-installs only when the toolchain version changed (the installed version is recorded in settings.toml). A failed extension install is a soft failure — the toolchain is what matters — and “no editor detected” is a quiet one-line note, not an error.
Editor support matrix
Two install mechanisms, by the contract the editor exposes. The user-facing detail — the language-server capability set, and the file-placement managed-config block — is in Toolchain → Editor support.
| Editor | Install mechanism |
|---|---|
VS Code (code), Cursor (cursor), VSCodium (codium), VS Code Insiders (code-insiders), code-server (code-server) | version-matched .vsix via the editor’s --install-extension CLI |
| Neovim, Vim, Emacs | file placement — embedded plugin files + an idempotent oxup-managed config block |
Neovim, Vim, and Emacs are recognized — naming one with --editor produces a specific, loud refusal pointing at manual setup and the tracking issue, never a silent no-op.
Runtime contract
A .oxbin is consumed by a runtime backend. The runtime contract defines what a backend promises: how to load an artifact, how to bind it to per-tenant state, how to answer queries against bitemporal data, how to apply mutations, and how to enforce the capability surface that gates compliance-sensitive operations.
The contract specifies a three-role factoring (Engine / Module / Store factoring), a trait surface (The OxbinRuntime trait surface), a lifecycle (Lifecycle), bitemporal AsOf semantics (AsOf semantics — bitemporal point), a capability surface (Capability surface), and the ox runtime serve HTTP surface (Serving surface).
Engine / Module / Store factoring
The runtime decomposes into three roles:
- Engine is the shared, immutable base — the shared schema (std + any base packages the workspace declared — including any vocabulary packages — plus the meta-axes those packages declared). The Engine is
Arc<SchemaStore>, mmap-friendly, and shareable across processes and threads. - Module is a typed, loaded
.oxbin. It owns the artifact’s structural content (symbol table, standpoint lattice, tier table, rules, queries, mutations) and is immutable for its lifetime. Many Stores may use one Module concurrently. - Store is the isolated execution context where facts live. One Store is one (tenant, fork) pair: it accumulates events through
mutateand answersquery.
A Store sees facts according to two filters: (1) tenant + fork scope — only events tagged with this Store’s tenant_id and fork_id, under the fork’s copy-on-write lineage rules (AsOf semantics — bitemporal point); (2) standpoint perspective — when a query names a standpoint, only events visible from that standpoint by the lattice ancestor relation.
The OxbinRuntime trait surface
A backend implements:
#![allow(unused)]
fn main() {
pub trait OxbinRuntime: Send + Sync {
type Module: 'static + Send + Sync;
type Store: 'static + Send + Sync;
type Error: std::error::Error + Send + Sync;
/// Load an .oxbin into a Module. Layer 1 validation runs synchronously
/// here (RP-004 hooks); Layer 2 runs in the prefix before this returns Ok.
fn load(
&self,
oxbin: &Oxbin,
validation: ValidationPolicy,
) -> Result<Self::Module, Self::Error>;
/// Open a Store for one (tenant, fork). May create the fork if it
/// doesn't exist (subject to capability checks per the backend's IAM).
fn open_store(
&self,
module: &Self::Module,
tenant_id: TenantId,
fork_id: ForkId,
) -> Result<Self::Store, Self::Error>;
/// Run a query at a bitemporal point (default `AsOf::Now`). Standpoint
/// scope is part of the query, not this argument.
fn query(
&self,
module: &Self::Module,
store: &Self::Store,
query: &CoreIRQuery,
as_of: AsOf,
) -> Result<Answer, Self::Error>;
/// Apply a mutation; returns the TT at which it became effective.
/// Mutations are transactional; partial mutations are not observable.
fn mutate(
&self,
module: &Self::Module,
store: &mut Self::Store,
mutation: &CoreIRMutation,
) -> Result<TxTime, Self::Error>;
/// Fork an existing (tenant, fork) at a given TT. The new fork inherits
/// parent state up to fork_point_tt; later mutations diverge (CoW, `AsOf` semantics — bitemporal point).
fn fork(
&self,
module: &Self::Module,
parent: &Self::Store,
new_fork_name: &str,
fork_point_tt: TxTime,
) -> Result<Self::Store, Self::Error>;
/// Capability-gated physical erasure (Capability surface). Requires both
/// `#[allow_forget]` on the target concept and the runtime principal
/// to hold the `forget` capability.
fn forget(
&self,
module: &Self::Module,
store: &mut Self::Store,
targets: &[AxiomId],
actor: PrincipalId,
reason: &str,
) -> Result<ForgetReceipt, Self::Error>;
}
}
The trait is Send + Sync; a backend’s concurrency discipline is its own concern. The ValidationPolicy argument to load distinguishes sandboxed Strict (any Layer-2 failure refuses the load) from trusted Lenient (Layer-2 failures emit warnings; non-load-bearing issues are tolerated).
AsOf, Capability, and ValidationPolicy are @[language_interface] types mirrored by oxc-protocol::runtime; CI fails on drift.
Lifecycle
A normal lifecycle: at startup, runtime.load(oxbin, policy) returns a Module (Layer 1 tier check synchronously, Layer 2 per-section invariants in the prefix); per request, open_store then query / mutate; at shutdown, Stores drop, then the Module, then the Engine.
A second load returns a new Module. The runtime MAY share the first Module’s Engine if the base schemas are byte-identical (Arc identity), otherwise the new Module gets a fresh Engine. Hot replacement of a live Module is not required by the in-process trait; the serving helper (Serving surface) implements a guarded form that loads the next Module, checks compatibility against live ABox state, and swaps atomically only if compatible. The Engine/Module split is what makes this possible without redesigning Store semantics.
AsOf semantics — bitemporal point
Every query reads facts at a specific bitemporal point. The AsOf type carries both axes:
#![allow(unused)]
fn main() {
pub enum AsOf {
/// Default: VT = now, TT = now — "what is currently true, as currently believed."
Now,
/// "What was valid at VT = t, as currently believed?" Historical-fact view.
AtVt(DateTime),
/// "What was believed at TT = t about VT = now?" Audit view.
AtTt(DateTime),
/// Full bitemporal: "what was believed at TT = tt about VT = vt?"
At { vt: DateTime, tt: DateTime },
}
}
The default AsOf::Now answers about current state; historical and audit queries are opt-in.
Forks interact with AsOf through the copy-on-write invariant. A child fork inherits parent state up to fork_point_tt. A query against the child at AsOf::AtTt(tt): if tt < fork_point_tt it reads the parent’s events at tt; if tt ≥ fork_point_tt it reads the parent’s pre-fork events plus the child’s own events up to tt. The parent’s pre-fork state is shared with all children; each child sees its own divergence. A backend MAY materialize the inheritance at fork-creation or compute it lazily on read; both satisfy the contract.
Capability surface
Three capabilities gate dangerous operations:
forget— physical erasure of axiom events for compliance (RP-004 Temporal substrate). The source-level gate: a mutate body containingforgetrefuses to build (OE0730) unless the enclosingmutatedeclaration grants#[allow_forget]. The serving layer refusesforgetoutright (Runtime contract limits below).unsafe_logic— execution oftier:folrules underunsafe logic { }. Granted to the workspace atox buildtime. A query whose dependency closure touches anunsafe_logicrule may run unbounded; runtimes MAY apply a per-query wall-clock timeout and returnOE1301 UnsafeLogicTimeout.fork— creating new forks, per-(tenant, parent_fork). A principal without it canopen_storeagainst existing forks but cannot create new ones. The trunk fork’s owner holdsforkautomatically.
Capabilities flow through the runtime as part of the PrincipalId argument to forget and the principal context of open_store. How a runtime authenticates a principal is backend-specific (the Postgres backend uses Postgres roles; the in-memory backend trusts the calling process).
Incremental evaluation
The runtime commits to incremental (Salsa-style) computation at two layers. A compile-time layer, keyed on the immutable .oxbin, memoizes Module-only work (parse / resolve / type-check / classify-tier / compile-rule) once per loaded Module and shares it across Stores. A runtime layer, one per (tenant, fork), memoizes every query whose dependency set touches the event log (extent, iof closure, rule fixpoint); a mutate bumps a per-Store generation counter and the engine invalidates exactly the downstream queries.
The load-bearing invariant: for any tracked query, the same (module, store generation, query args) yields the same result. The backend MUST guarantee the generation bump after a successful mutate is atomic and monotonic — no thread observes the increment before the mutation is durable (for Postgres, the bump is in the same transaction as the event insert; for in-memory, a write-lock spans the index update and the increment). Per-(tenant, fork) keying — not per-tenant — is what keeps a mutation on fork A from invalidating warm queries on fork B.
Serving surface
The in-process trait is the semantic contract. ox runtime serve is the first-party helper around it for tools that need a process boundary, specified by RFD-0014. It exposes a versioned HTTP surface:
| Route | Meaning |
|---|---|
GET /v1/health | service status, storage mode + reachability, active module hash, artifact schema hash |
GET /healthz | liveness — process is up (does not probe storage) |
GET /readyz | readiness — probes storage; 503 when the backend is unreachable |
GET /v1/module | active module status, source path, diagnostics, artifact schema hash |
GET /v1/schema | generated-schema-equivalent TBox metadata, plus the artifact schema hash a generated SDK checks for version skew |
POST /v1/dispatch/query | execute a declared pub query descriptor |
POST /v1/dispatch/mutation | execute a declared pub mutate descriptor |
POST /v1/dispatch/compute | execute a supported pure pub fn descriptor |
POST /v1/batch | execute an ordered array of mutation dispatches atomically |
POST /v1/forks/{fork}/dispatch/* | fork-scoped descriptor dispatch |
GET /v1/forks, POST /v1/forks | list and create forks |
GET /v1/forks/{fork} | inspect one fork record |
DELETE /v1/forks/{fork} | abort a fork |
GET /v1/forks/{fork}/diff/{other} | compare visible ABox events |
POST /v1/forks/{fork}/promote | promote fork-local events into main |
POST /v1/forks/{fork}/derive | run buffered derive projection |
POST /v1/forks/{fork}/derive/trace | return buffered derive trace events |
GET /v1/snapshot | materialize TBox, ABox (paginated), and optional derivations |
GET /v1/checks | enumerate currently-violated checks in a scope |
GET /v1/derived/individuals/{id} | derived facts mentioning an individual |
GET /v1/derived/individuals/{id}/explain | explanations for that individual’s derived facts |
GET /v1/derived/facts/{fact_id}/explain | explanation for one derived fact |
Dispatch routes take a runtime context from trusted headers (X-Tenant-Id, X-Fork-Id, X-Principal-Id, X-Request-Id, X-Standpoint) or the request body; headers win when both are present. Missing tenant defaults are a CLI/helper concern; missing fork defaults to main.
Storage modes. mem keeps Store state in process memory — useful for local experiments, tests, and short-lived sandboxes; durability ends with the process. It is the default runtime backend. pg is an append-only axiom event log in Postgres (oxc-storage-pg): scoped tenant/fork/standpoint/module scans, AsOf::Now/AtVt/AtTt/full-bitemporal reads, durable fork records and lineage, generation counters bumped atomically with appends, projection-cache invalidation in the same transaction, and decoded body-field lookups for iof assertions and relation tuples. ox runtime serve --storage pg selects it. The source of truth in either mode is the event log; snapshots, derived facts, and projection caches are recomputable views over .oxbin plus visible events.
Ad-hoc query/mutation submission (RFD 0033). Beyond declared-descriptor dispatch, the API accepts arbitrary query/mutation bodies as source text — POST /v1/query/adhoc and POST /v1/mutation/adhoc (plus the per-fork analogs). A submitted body is parsed, fully type-checked against the loaded module (the same checker the build runs, via the runtime Schema backend — an ill-typed body is refused with ARGON_RUNTIME_ADHOC_TYPE_ERROR and never executes), lowered, and run through the same path declared pub query/pub mutate use; the declared forms are a named convenience layer over this generic path. Ad-hoc invocation carries the same context/tenant/fork/standpoint scoping as declared dispatch — there is no ad-hoc-specific restriction on what it may read or write, with one capability exception: forget (physical erasure) requires the build-time #[allow_forget] grant, which a runtime-submitted body cannot self-confer, so an ad-hoc forget is refused (OE0730). Ad-hoc submission is on by default; a deployment may lock down to declared invocables only (--no-adhoc / --no-adhoc-mutation), in which case the endpoint returns ARGON_RUNTIME_ADHOC_DISABLED.
Generic entity writes remain declaration-typed. There is no untyped entity-write surface (no POST /v1/entities): every write — declared or ad-hoc — routes through the typed pub mutate / Operation mechanism, never a raw entity blob. Compute dispatch supports the pure CoreIR subset evaluable without Store effects (literals, parameters, tuples, lists, boolean/int/text operators, membership, conditionals, ascription); calls, projections, and Store-dependent terms are refused until their runtime substrate is specified.
Entity-reference arguments — new vs existing. A pub mutate parameter typed as a declared concept/struct is an entity reference. Over HTTP such an argument takes a JSON string in one of two forms, and the form the client chooses decides the meaning:
- An
#i<N>string (e.g."#i7") names an existing individual — one already minted in this scope (typically returned by an earlier dispatch). It is validated against the store’s next-fresh floor: a reference at or above the floor cannot name an existing individual and is refused (ARGON_RUNTIME_VALIDATION_FAILED). - Any other string (e.g.
"alice") is a client-supplied symbolic name for a NEW individual. The server mints a fresh#i<N>identity for it inside the per-scope critical section, runs the mutation with that identity substituted, and returns the mapping on the receipt undermintedEntities: an array of{ "name", "id", "concept" }records. The client uses the returnedidto refer to that individual on subsequent dispatches.
This is what makes the flagship models writable over HTTP: a mutation such as openRentObligation(landlord: Person, tenant: Person, …) whose body classifies its parameters via insert iof(landlord, Person) is dispatched by supplying symbolic names ("alice", "bob"), and a later recordPayment(satisfactionAccount: SatisfactionAccount, payment: SatisfactionRecord, …) supplies the minted #i<N> for the existing account and a fresh symbolic name for the new payment. The in-process / demo.toml path mints symbolic names deterministically from the name (so the same name is the same individual); the HTTP path mints fresh per request with no idempotency-by-name — the same name in two requests is two individuals.
Atomic batches. POST /v1/batch executes an ordered array of mutation dispatches as one unit: { "context": …, "mutations": [ { "qualifiedPath", "args" }, … ] }. Every step runs in one per-scope critical section against a single hydrated store, so step N sees the effects of steps 0..N and the check delta-guard evaluates each step against the accumulating state. On success the union of emitted events is persisted in one transaction; the response carries committed: true, a per-step steps array (each with its result / mintedEntities / diagnostics), and eventsCommitted. Any step failure rolls the whole batch back — nothing is persisted — and the error envelope reports the failing step under error.details.batchStep. The request body cap and the result-row cap apply to the batch as a whole. New-entity symbolic-name minting is per step: a symbolic name repeated across two steps mints two individuals (each step is a distinct mutation). To thread one new individual through several steps, mint it in the first step and reference the returned #i<N> in the later ones.
Named individuals in outputs. Individuals declared by pub fact carry their source name into the artifact’s name-index, and every entity rendered by a query / extent / snapshot / dispatch result includes that name under $name alongside the opaque $id (so a served scope reads with its modeler-chosen names). HTTP-minted individuals (above) carry their client label out on the dispatch receipt’s mintedEntities; the durable identity is always the #i<N> id.
Runtime error codes. Failures on the /v1 surface use a stable error.code string in the ARGON_RUNTIME_* family (distinct from the compile-time OE#### catalog of Appendix C). Every 4xx/5xx response uses the same structured envelope — {error:{code,message,details},requestId,moduleHash} — including routing, method, and request-body parse failures (a malformed JSON body is ARGON_RUNTIME_VALIDATION_FAILED, an unknown route is ARGON_RUNTIME_UNKNOWN_ROUTE, a wrong method is ARGON_RUNTIME_METHOD_NOT_ALLOWED), so a client keys on error.code uniformly for every failure class. The full set of codes, their HTTP statuses, and their meanings is the published failure reference in Appendix E, checked against the runtime’s canonical registry. A batch-step failure rides the same envelope with the failing step index under error.details.batchStep.
Pagination. A list-shaped read (pub query, /v1/snapshot) larger than the result-row cap is read page by page rather than refused. A query takes an optional page: { limit, cursor } in its request body; /v1/snapshot takes ?limit=&cursor=. The response carries a page object — { total, returned, nextCursor } (the snapshot’s under abox.page) — and a nextCursor of null marks the last page. The cursor is an opaque server-issued token: pass back the previous page’s nextCursor verbatim, do not parse or construct it (a foreign cursor is refused with ARGON_RUNTIME_INVALID_CURSOR). Page size is min(limit, cap), so one page never exceeds the cap; an un-paged read still refuses (ARGON_RUNTIME_RESULT_TOO_LARGE) when the whole result is over the cap — a truncated answer is indistinguishable from a complete one.
The cursor pins a read point, so a multi-page walk is consistent against one fixed view rather than re-evaluated against live state each page. Page 1 reads at the request’s asOf (or Now); the server resolves Now to the transaction time of the store that served the page and embeds it in the nextCursor. Every later page inherits that pinned point from the cursor (it overrides the request’s asOf), so the offset is always taken against the same bitemporal snapshot. A concurrent insert lands past the pinned point and stays invisible to the walk; a concurrent retraction before the cursor stays visible (it existed at the pinned point) — so the walk neither skips nor duplicates a row, and total is stable across pages. To page over a historical point, pass asOf (the bitemporal read point of AsOf semantics — bitemporal point) on the first request; the whole walk then follows it.
/v1/snapshot carries the same pin in its cursor. On the durable pg backend the pinned page re-hydrates the ABox at the pinned transaction time, so the snapshot walk is consistent under concurrent writes. The in-process mem backend keeps only current-state events (it has no historical scan), so a mem snapshot walk is a live positional read; production serving is pg-backed.
Checks surface. GET /v1/checks enumerates the checks currently violated in a scope, evaluated against live state — the compliance/violations view, complementing the per-mutation observe channel (which only fires on a write). The response carries violated (any blocking Error firing), violationCount, diagnosticCount, and a diagnostics array of {code,message,severity,check,args} (the same shape a mutation response’s observe-channel diagnostics use). ?check=<name> filters to one check (an unknown name is ARGON_RUNTIME_UNKNOWN_CHECK). The generated SDK exposes this as client.checks(), and the mutation receipt now carries the observe-channel diagnostics (previously dropped for typed-SDK consumers).
Liveness and readiness. GET /healthz is liveness — 200 while the process is up and serving; it deliberately does not probe storage, so a transient storage outage does not trigger a process restart. GET /readyz is readiness — it probes the storage backend (SELECT 1 on pg; always ready on mem) and returns 503 ARGON_RUNTIME_STORAGE_UNREACHABLE when storage is down, so a load balancer stops routing to a dead-storage instance. GET /v1/health folds both into one human/debug body and is honest about storage: ok:false + 503 when the backend is unreachable. See the deployment runbook for how an orchestrator should wire these.
Hot reload. The serving layer may watch the active .oxbin path. On a changed artifact it loads the new bytes into a fresh Module, validates, computes the new module hash and declaration signatures, probes live ABox state, accepts additive schema changes, and rejects declaration removal or changed signatures when live ABox state exists — keeping the previous Module active and recording diagnostics on failure. Failed reloads never discard ABox state.
Capability and provenance limits. The serving layer enforces fork on nested forks. forget mutations are refused at the serving layer; unsafe_logic queries are refused (lowering emits unsafe_logic = false for supported rules). The derive/explain endpoints expose buffered rule/tuple provenance only; full PosBool DNF witness trees are reserved for the formal provenance substrate and must not be implied by the response shape.
Defaults
Runtimes default several axes — these live in the ox CLI driver and library helpers, not in the runtime trait: tenant → a singleton UUID (multi-tenant configs override); fork → trunk at tenant creation; standpoint → tenant-wide :root; AsOf → Now; ValidationPolicy → Lenient for ox run, Strict for sandboxed contexts.
SDK generation
ox gen emits a typed client SDK from a built .oxbin so application code calls declared pub query / pub mutate / pub fn descriptors with checked argument and result types instead of hand-writing HTTP requests against the /v1 surface.
Mechanization
The runtime contract is mechanized in Lean as abstract signatures (parallel to std::kripke’s interface-only mechanization): Engine/Module/Store types, the OxbinRuntime trait surface, the bitemporal AsOf type, the three-capability surface, and the generation-monotonicity / cache-correctness invariant, under Argon/Runtime/. oxc-protocol::runtime mirrors the @[language_interface]-tagged types; CI fails on drift.
Deployment and trust model
The server has no authentication. ox runtime serve consumes X-Tenant-Id / X-Principal-Id (and the other context headers) at face value — it does not verify who the caller is or whether they may act as the named tenant or principal. Tenant data is isolated at the storage layer (one tenant cannot read another’s individuals, verified), but the gate deciding who may claim a tenant is absent by design. A caller that sets X-Tenant-Id: acme reads and writes acme’s data regardless of any Authorization header.
This is an honest internal-service posture, not a bug — but it has a hard deployment consequence: the server MUST be fronted by a gateway and MUST NOT be directly reachable by untrusted clients. The default loopback-only bind (serve refuses a non-loopback host) enforces “behind a proxy on the same host or network namespace”; do not relax it without a fronting gateway.
The deployment contract:
- Front it with an authenticating gateway. A reverse proxy / API gateway terminates client auth (mTLS, OIDC, an API-key service — backend-specific), and only after authenticating injects the trusted
X-Tenant-Id/X-Principal-Idheaders. The runtime trusts these because the gateway is the only thing that can reach it. - Strip inbound trust headers at the edge. The gateway MUST overwrite (not pass through)
X-Tenant-Id/X-Principal-Id/X-Standpointfrom the client request, so a client cannot forge a tenant. Treat these as gateway-internal headers. - Network-isolate the runtime. Bind loopback (the default) or a private interface, and use network policy / security groups so only the gateway can open a connection. The runtime has no rate limiting or load-shedding beyond the per-request timeout, body-size cap, and result-row cap (Serving surface) — the gateway owns rate limiting and client back-pressure.
- Wire liveness/readiness to the orchestrator. Point the orchestrator’s liveness probe at
GET /healthz(restart on failure) and its readiness probe atGET /readyz(remove from the load-balancer pool on failure — e.g. when Postgres is unreachable). Do not gate liveness on storage: a storage outage should drain traffic (readiness), not restart pods (liveness). - Schema-change gate. Cross-version artifact loads over live data refuse by default (
ARGON_RUNTIME_SCHEMA_MISMATCH, R-B7); an operator opts into serving a changed schema over an existing scope withARGON_ACCEPT_SCHEMA_CHANGE=1. Treat that as a deliberate migration step, not a default.
forget (physical erasure) is refused outright over HTTP and there is no per-principal authorization inside the runtime (Capability surface, Serving surface) — all authorization is the gateway’s responsibility.
Embedding contract (in-process)
oxc-runtime and oxc-serve are publish = false: there is no crates.io artifact, no semver guarantee, and no docs.rs. Embedding the runtime in-process from Rust means a git dependency on the workspace against APIs that are documented at intent level (the crates’ AGENTS.md nodes) but are not a stable, versioned contract. The semantic contract is the OxbinRuntime trait (The OxbinRuntime trait surface); the serving surface (Serving surface) is the supported boundary for everyone else. Prefer the HTTP surface unless you specifically need in-process embedding.
The in-process Store is the mem backend: durable Postgres serving is reached through the served async adapter (PgOxbinRuntime), via ox runtime serve --storage pg, not by constructing a Store and selecting pg. The HTTP serving surface (Serving surface) sits above the in-process surface — it uses the served pg adapter rather than in-process Store pg — so an integration over HTTP is the lower-risk path.
Storage layer
Argon’s storage layer is a trait abstraction (The storage trait) with two backends behind it: an in-memory backend for tests and REPL, and a Postgres backend. Both implement the same trait so the rest of the runtime is backend-agnostic.
The architectural decision that frames every other choice is the rejection of triple-decomposition: storage is a single append-only log of axiom-ADT events, not entity-attribute-value triples (The single-event-log architecture). The single log carries every cross-cutting concern — discriminator axes, bitemporality, defeasibility, provenance, capability gates — once, instead of per-relation.
The contract is ontology-neutral. No UFO axis is a column. Rigidity, sortality, identity-provision, and any vocabulary-specific axis live as meta_property events whose body declares (axis, target, value). A UFO vocabulary package ships its axes as data, not as type-system primitives.
The single-event-log architecture
Storage is one canonical relation: axiom_events. Every kernel write — concept declaration, subsumption axiom, relation declaration, rule declaration, type assertion, relation tuple, property assertion, meta-property, standpoint declaration, module declaration, retraction — inserts exactly one row. Rows are never updated in place except for the transaction-time-end field on supersession. Rows are deleted only via the capability-gated forget operation (Capability surface).
Each row carries:
- Identity — a UUIDv7 event identifier (time-orderable, monotonic) and a stable
axiom_idUUID identifying the logical axiom across edits. - The axiom-ADT body — a variant tag (one of the 26 axiom kinds catalogued in Axiom-ADT variant catalog) plus a canonical-CBOR payload mirroring Argon’s CoreIR 1:1. The
content_idisSHA-256(body), so identical bodies deduplicate. - Discriminator axes — tenant, fork, standpoint, module. Always present; default singletons in single-tenant deployments.
- Bitemporality — a valid-time range and transaction-time begin/end (Temporal substrate, RP-004).
- Polarity — assert (
+) or retract (-); retractions point at the assert they supersede. - Decidability classification — the tier-pair
(main_tier, temporal_tier)per Tier ladder and RP-004. - Defeasibility proof tags — Governatori-Rotolo 4-slot tags per defeasible reasoning.
- Provenance — PosBool(M) DNF for derived events (absent for axioms).
The four discriminator axes plus the bitemporal pair give the 4-axis versioning contract: any event is located by (tenant, fork, standpoint, module) in space and by (valid-time, transaction-time) in time. A query at an AsOf point (AsOf semantics — bitemporal point) reads the events live in scope at that bitemporal coordinate.
Why one log, not many tables. A split design (mutable normalized TBox tables plus an append-only ABox log) cannot answer “what was the TBox as of TT₀” without rebuilding state from a separate ledger, and forces every discriminator axis to be added redundantly to every TBox table. A unified log factors these axes once. Event sourcing is the correct foundation for bitemporal retraction, copy-on-write forking, and PosBool(M) derivation provenance; any of these over a mutable schema introduces impedance mismatches that surface as edge-case correctness bugs.
Why ADT-shaped bodies, not RDF-style decomposition. Reifying an axiom into triples loses the ADT pattern-matching benefit, inflates row count 5–50×, and adds a join layer on every read. The empirical case is clear: horned-owl (Phillip Lord, Newcastle) achieves 1–2 orders of magnitude faster parsing and ~20× less memory than the OWL API’s triple store by using Rust enums for OWL axioms; Konclude and Tawny-OWL converge on the same shape. Pattern matching on the ADT is reasoning in the matching-logic sense (Roșu 2017, LMCS).
The body encoding is canonical CBOR (deterministic per RFC 8949 §4.2.1). The body schema is the variant’s body shape from the axiom-ADT variant catalog.
The storage trait
Backends implement:
pub trait StorageBackend: Send + Sync {
type Error: std::error::Error + Send + Sync;
/// Open or create storage for a (tenant, fork). Includes any
/// per-backend schema migrations.
fn open(
&mut self,
tenant_id: TenantId,
fork_id: ForkId,
) -> Result<(), Self::Error>;
/// Append one axiom event (assert or retract).
fn append(&mut self, event: &AxiomEvent) -> Result<TxTime, Self::Error>;
/// Append a batch of axiom events as one transaction.
fn append_batch(
&mut self,
events: &[AxiomEvent],
) -> Result<TxTime, Self::Error>;
/// Physically erase axiom events for compliance. Capability checked
/// by the caller; the backend logs to its forget log.
fn forget(
&mut self,
targets: &[AxiomId],
actor: PrincipalId,
reason: &str,
) -> Result<ForgetReceipt, Self::Error>;
/// Scan live events of a kind in scope at a bitemporal point.
/// Returns events with op='+' and (tx_to absent OR tx_to > as_of.tt).
fn scan_live(
&self,
scope: Scope,
kind: AxiomKind,
as_of: AsOf,
) -> Result<EventCursor<'_>, Self::Error>;
/// Lookup events by extracted body field. Backends with the right
/// index serve this in O(log n); others fall back to scan.
fn lookup_by_field(
&self,
scope: Scope,
kind: AxiomKind,
field: BodyField,
value: BodyValue,
as_of: AsOf,
) -> Result<EventCursor<'_>, Self::Error>;
/// Per-axiom-id history (across retractions / reasserts).
fn axiom_history(
&self,
scope: Scope,
axiom_id: AxiomId,
) -> Result<EventCursor<'_>, Self::Error>;
/// Apply CQRS projection maintenance after a batch of appends.
fn maintain_projections(
&mut self,
delta: &EventDelta,
) -> Result<(), Self::Error>;
}
Scope carries (tenant_id, fork_id, standpoint_id_set, module_id_set). The standpoint_id_set is typically a single standpoint plus its lattice ancestors (pre-computed at compose time, cached in global-control).
Backends manage their own schema versioning. The schema version is not carried in the .oxbin preamble.
The Postgres backend
The Postgres backend (oxc-storage-pg) materializes the event log as a single canonical axiom_events table plus a set of CQRS projection tables (modules, concepts, relations, meta-axes, standpoints and their ancestry, per-(standpoint, concept) world assumption, forks, and a forget-audit log). The event log is the source of truth; projections are recomputable views maintained incrementally by the Argon runtime calling maintain_projections after each batch (so projection logic stays in Rust and is shared with the in-memory backend) rather than by DB triggers.
Body fields are extracted into DB-enforced generated columns (STORED GENERATED ALWAYS AS) via an oxc-pgx pgrx extension that reads canonical CBOR; because deterministic CBOR is byte-deterministic, these extractions cannot drift from the body. Hot-path access patterns — live-in-scope-by-kind, axiom-id history, valid-time and transaction-time ranges, and per-variant / per-relation hot fields — are served by partial, GiST, and BRIN indexes that ox build auto-generates from the declared schema; modelers never write storage indexes by hand.
The concrete Postgres DDL — table definitions, index definitions, the cbor_extract_* extension signatures, and the projection catalog — is an operational artifact of this one backend, not part of the language surface. It is not mechanized (see Mechanization) and lives in the oxc-storage-pg crate. ox kernel init installs the extension and runs migrations.
The Postgres backend crate exists but is not the default runtime path; the in-memory backend is what the runtime exercises by default (see Serving surface).
The in-memory backend
The in-memory backend (oxc-storage-mem) is the live backend — for tests, REPL, and ephemeral runtimes. It implements StorageBackend with BTreeMap-backed indexes plus an interval tree for O(log n) bitemporal range queries, sharing all query and mutation code with the Postgres backend. It is the default for cargo nextest run -j 4 (tests need no Postgres). It has no durability (drops on shutdown), no IAM (trusts the calling process; capability checks pass unconditionally), and no partitioning.
Mechanization
The storage layer is mechanized in Lean at the contract level — abstract over backend, ontology-neutral, axiom-ADT-shaped:
Argon/Storage/Trait.lean— abstractStorageBackendsignature (no DDL)Argon/Storage/AxiomEvent.lean— the canonical event row shape (@[language_interface])Argon/Storage/AxiomKind.lean— the 26 variants as a Lean inductive (@[language_interface])Argon/Storage/AxiomBody.lean— per-variant body payloads (@[language_interface])Argon/Storage/CatalogProjections.lean— projection shapes
oxc-protocol::storage mirrors the @[language_interface]-tagged inductives; CI fails on drift. The Postgres DDL is not mechanized — it is an operational artifact of the Postgres backend. The trait admits future backends (analytical / RDF-bridge / property-graph / object-store) without changing the canonical axiom_events abstraction; none are standard backends, and none would change the event-log contract above.
Stdlib (selected)
Standard packages
The standard library is five packages. The compiler embeds each one’s root.ar via include_str! (oxc-instantiate/src/lower.rs) and elaborates it before every user build, so use std::… resolves to real declarations.
| Package | Provides |
|---|---|
std::core | Top, Bot, the generic metatype type, the generic metarel rel. Truth4 / Truth4Of<T> are compiler-injected (in scope without a use). |
std::rel | The relation-property library macros (transitive, irreflexive, asymmetric, functional) — see Migration: re-homing the hard-coded surface. |
std::kripke | World, Entity, accessible decls — vocabulary for the modal evaluator (see Modal operators). |
std::path | Path<NodeT, EdgeT>, Edge<A, B>, Weighted trait — type decls (see The Path type). |
std::temporal | A Temporal trait anchoring the namespace (see Temporal rule atoms). |
Higher-order type theories are not in std. MLT ships as the unprivileged first-party package mlt (packages/mlt, RFD 0043): its own metarels (categorizes, partitions, is_subordinate_to, power_type_of), the order metaxis, and the decorator #[procmacro]s, with zero compiler privilege — a consumer lists it in ox.toml and imports the vocabulary. Parallel theories (potency, ML2, …) ship the same way; the substrate stays neutral about which one a model commits to.
Per-package worked programs live in examples/, compiled and run in CI.
Note std::math is not a package. The numeric and temporal types (Nat, Int, Real, Decimal, Money, Date, …) are compiler-injected primordials resolved directly by the resolver, not imported from .ar source. The fixed-width primitives (i8–i128, u8–u128, f32, f64) are likewise compiler-provided under the std::math::primitive name, requiring explicit use.
Primordial types (lexically always in scope, no use required):
Nat, Int, Real, Decimal, Money, Date, Time, DateTime, Duration,
Bool, String, Top, Bot
These match how ontologists think — counts, measurements, monetary amounts — rather than machine-level widths. Fixed-width variants (i8–i128, u8–u128, f32, f64) ship in std::math::primitive for FFI, performance-critical paths, and binary-protocol interop; explicit use required.
The prelude is a package feature, not a compiler default (RFD 0038). There is no auto-imported std::prelude; a package’s [package].prelude (ox.toml, an array of use-tails) auto-prepends uses to its own modules, empty by default, opt-out per-module with #![no_implicit_prelude]. What is always in scope without any use is the substrate — the language primitives, not a library:
- Primordial types (above) and builtin type forms:
Option<T>(Some/None),Result<T, E>(Ok/Err),Ordering(Less/Equal/Greater),Truth4,Truth4Of<T>,List<T>,Set<T>,Map<K, V>,Range<T>, plus the check surfaceDiagnostic/Severity. These are compiler-provided (is_builtin_type_form) and resolve directly — in scope everywhere, no import, no prelude.
type/rel are not ambient. Opt into the no-commitment baseline with use std::core::{type, rel} or a [package].prelude entry (RFD 0038) — exactly as you bring in any vocabulary. Every ontological commitment is visible in the source: vocabulary classifiers (UFO’s kind, BFO’s class, …) and the baseline type/rel alike resolve only against pub metatype / pub metarel declarations in scope — declared in the package or imported from an external vocabulary package (Name resolution, Vocabulary concepts and the generic type metatype); an unresolved introducer is OE0605 / OE0606. No vocabulary — not even the baseline — ships ambient. A vocabulary package may re-export std::core::rel (and/or type) in its own prelude.ar as a convenience.
Numeric tower and coercion
Modeling-surface numeric types (primordial, always in scope):
| Type | Meaning |
|---|---|
Nat | unsigned integer; arbitrary-precision logically, runtime-chosen width |
Int | signed integer; arbitrary-precision logically, runtime-chosen width |
Real | real number; exact arbitrary-precision rational (RFD 0016). Inexact floats are the opt-in f32/f64 in std::math::primitive. |
Decimal | arbitrary-precision decimal (exact arithmetic) |
Money | currency-typed decimal, exact (cent precision by default) |
All numeric primitives are signed where signedness is meaningful. The runtime chooses an implementation width based on refinement bounds: Int where _ < 256 may compile to i16; unbounded Int defaults to arbitrary-precision.
Fixed-width primitives (std::math::primitive, requires explicit use):
i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64
For FFI, performance-critical paths, and binary-protocol interop.
Money arithmetic (per D-069):
| Operation | Result |
|---|---|
Money + Money | Money |
Money − Money | Money |
Money × (Nat | Int | Real | Decimal) | Money |
(Nat | Int | Real | Decimal) × Money | Money |
Money ÷ (Nat | Int | Real | Decimal) | Money |
Money ÷ Money | Decimal |
Scalar multiplication and division accept any numeric type as the non-Money operand (the scalar widens implicitly to Decimal for the arithmetic). Money-to-Money addition and subtraction are currency-checked at elaboration when the operands have refined currency annotations. No implicit conversion Nat → Money or Decimal → Money; construct via Money::from_cents(n) or Money::from_decimal(d, currency).
Temporal arithmetic:
| Operation | Result |
|---|---|
Date + Duration | Date |
Date − Date | Duration |
Date − Duration | Date |
DateTime + Duration | DateTime |
DateTime − DateTime | Duration |
Time + Duration | Time |
Date literals. A calendar date is written hash-delimited: #YYYY-MM-DD#. The hashes are load-bearing — they distinguish a date from arithmetic. A bare 2024-01-01 is integer subtraction (2024 − 1 − 1 = 2022), not a date; written against a Date-typed position it is refused with OE1340 and a directed hint toward the #…# form (the #date# silent-Int landmine, fixed in arc5). The inner text is an ISO YYYY-MM-DD civil date validated at compile time (month 01–12, day in range, leap years exact); a DateTime literal #YYYY-MM-DDTHH:MM:SSZ# carries a trailing T time.
pub type Invoice { mut issued: Date, mut paid: Date }
// `<int>.days` / `<int>.weeks` are duration sugar: a fixed whole-day count
// (`30.days` ⤳ 30, `2.weeks` ⤳ 14). `Date ± Duration` evaluates exactly.
pub derive overdue(i: Invoice) :- Invoice(i), i.paid > i.issued + 30.days;
// `today()` reads the evaluation's current valid-time (a `Date`).
pub derive past_due(i: Invoice) :- Invoice(i), i.paid < today();
Duration construction. A duration is a count of whole days in this increment. The fixed-length units <int>.days and <int>.weeks lower at rule-term resolution to a Duration literal — no impl is needed, and they work in rule bodies today. The calendar-relative units .months / .years have no fixed day count (a month is 28–31 days depending on the anchor), so they are loud-refused with OE1337 rather than silently approximated; compute the target date explicitly instead. Sub-day units (.seconds / .minutes / .hours) await the sub-day Time value layer.
today() — the current valid-time of the evaluation, as a Date — is the one nullary date intrinsic that evaluates today (now() awaits the sub-day layer). It is fixed for the whole evaluation, so a rule stays a pure function of its snapshot; a served model’s deadline arithmetic advances with the wall clock across requests rather than baking a build-time date.
Operator semantics — division. / is exact field division on the numeric tower, on every plane (checker, runtime, reasoner). Statically, Int / Int : Real — the quotient of two integers is a rational, and the type says so; 7 / 2 evaluates to the exact rational 7/2, never a truncated 3. The inference is uniform: 20 / 5 : Real statically even though the value collapses at the carrier (next sentence) — soundly, since Int <: Real. At the value carrier, integral results collapse to Int per the tower-wide convention (20 / 5 is Int(4); integral Real values are Int at the carrier). All other arithmetic operators on Int × Int stay Int. Truncating integer division does not exist; if it is ever wanted, it will be an explicit operator, never /.
Coercion rules:
- Implicit widening:
i32 → i64 → Int,Nat → Int, andInt → Real. SinceRealis exact (RFD 0016),Int → Realis exact-into-exact (Int ⊆ Realmathematically):require perPeriodValue > 0compares theRealagainst theIntliteral0without an explicit0.0, andInt + Realyields aReal; a pure-Intexpression staysInt— except/, which isReal(operator semantics above). The inexact floatsf32/f64do not implicitly widen intoReal— a float reaches the exact tier only via an explicit, lossy conversion (widening an approximate value into the exact tier must be a visible choice). - Explicit narrowing:
i64::try_into::<i32>()returningResult<i32, OverflowError>. - No implicit truncation, ever.
Per RFD 0016, Real/Decimal/Money are exact (arbitrary-precision rational); the reasoner folds them exactly (so avg over a Real collection is exact). Custom-precision fixed-point and refinement-driven width selection are out of scope (Out of scope).
Effective-dating: valid time and the two as_of axes
Argon’s substrate is bitemporal (RP-004): every assertion carries two orthogonal time axes.
- Transaction time (TT) — when the system was told. The append-only event log’s stamp; an
as_of <int>query reads “what the system would have said at TT = t” (the tx-counter axis). Audit/replay. - Valid time (VT) — when the fact held in the world. Independent of when it was recorded; an
as_of <#date#>query reads “what was VALID at VT = t, as currently believed.” Effective-dating.
The two are independent and composable: a query may pin TT alone, VT alone, or both (the full bitemporal point). They answer different questions — “what did we believe last Tuesday” (TT) versus “what rate was in effect on 2024-04-15” (VT) — and a model that conflates them (a hand-rolled effectiveFrom: Date field) cannot express the second cleanly.
Asserting at a valid time. A write carries an optional at <date> qualifier; the assertion’s valid time begins on that civil day (midnight UTC) rather than at write time. Both the imperative mutate-body form and the declarative pub fact form take it:
pub type StandardRate;
// effective-dated enactment: the membership holds FROM `effective` onward
pub mutate enact(r: StandardRate, effective: Date) {
insert iof(r, StandardRate) at effective;
}
// the declaration-form twin — a top-level effective-dated assertion
pub fact StandardRate(rate2024) at #2024-01-01#;
The at <date> expression must evaluate to a Date (a #date# literal or a Date-typed value); a bare arithmetic value is refused (OE1339). The window form during <window> and bitemporal retraction (a valid-time-qualified delete) refuse loudly (OE1330) rather than silently applying at all valid times.
Reading at a valid time. An as_of <#date#> clause on a query projects the extent valid on that civil day:
pub type Rate;
// "what rate(s) were in force on this filing day?" (the VALID-time axis)
pub query rate_on_filing_day() -> Rate as_of #2024-04-15#;
// "what the system believed at TT = 5 about VALID = now" (the TX-time axis)
pub query rate_at_tx_5() -> Rate as_of 5;
A Rate enacted at #2024-01-01# is invisible to an as_of #2023-…# read and visible to an as_of #2024-…# read — the effective-dating round-trip. See examples/effective_dated_tax_v0 for the full rate-table transcript.
Reserved keywords
Lexer-level reserved words cannot be used as identifiers. The table below is the complete set, generated from the grammar’s keyword table; a keyword carrying a clarifying note shows it inline.
| Keyword | Note |
|---|---|
abstract | RFD 0027 D6 — substrate-neutral declaration modifier: no direct instances. Legal on metatype declarations (every introduced type is abstract) and per-type on concept declarations (pub abstract type Vehicle). Instantiability for the OE1327 coverage gate is NOT-abstract — the compiler reads this flag, never a user axis name. |
across | |
acyclic | |
all_shortest | |
always | |
ambiguous | |
any | |
as | |
as_of | Bitemporal time-travel query clause. pub query foo() -> T as_of <expr> filters events by transaction time at query execution. Wire-encoded as an Option<CborValue> on QueryDeclBody. |
asc | |
assert | |
async | |
at | |
await | |
both | |
box | |
box_minus | |
box_plus | |
bridge | |
by | |
check | |
const | |
delete | |
derive | |
desc | |
detach | |
diamond | |
diamond_minus | |
diamond_plus | |
do | |
during | |
else | |
emit | |
enum | |
ever | |
except | |
exists | |
expect | |
fact | |
false | |
fixed | RFD 0027 D6 — substrate-neutral declaration modifier: classification decided at construction. Metatype-level only (pub fixed metatype kind = { … };). insert iof / delete iof against a type introduced by a fixed metatype refuses (OE0234); construction is unaffected. Dynamic classification is the default; fixed is the opt-in restriction. |
fixture | |
fn | |
for | |
forall | |
forget | |
from | |
group | |
having | |
if | |
iff | |
impl | |
in | |
insert | |
intersect | |
into | |
iof | |
is | |
k_shortest | |
let | |
limit | |
logic | |
macro | |
mapping | |
match | |
meta | |
metarel | |
metatype | |
metaxis | |
mod | |
mut | |
mutate | |
not | |
not_fact | RFD 0010 — strong (classical) negation of facts. Distinct from not (NAF in rule bodies, no wire trace) — pub not_fact P(x) is a declaration that lowers to an IofRefutation / RelationTupleRefutation axiom event. |
offset | |
on | |
optional | |
or | |
path | |
pub | |
query | |
require | |
return | |
select | |
Self | |
self | |
set | |
shortest | |
simple | |
since | |
sink | |
specializes | |
standpoint | |
struct | |
test | |
timeout | |
trail | |
trait | |
true | |
union | |
unknown | |
unsafe | |
until | |
update | |
upsert | |
use | |
walk | |
where | |
with |
Reserved for future use (parse but currently unimplemented): async, await, do, or.
Plus type-level constants and primordial types (always in scope, see the standard library): Top, ⊤, Bot, ⊥, Nat, Int, Real, Decimal, Money, Bool, String, Date, Time, DateTime, Duration.
Formerly reserved. mode and ordered are no longer keywords (RFD 0031 D7): both were vestigial reservations with no parser rule consuming them, and mode blocked a common vocabulary word (pub metatype mode). The documented graph-traversal mode clause (mode-spec, the rule atom) is recognized contextually in the role-step position when that surface lands, never as a reserved word. The Allen interval relations (before, meets, met_by, overlaps, overlapped_by, contains, starts, started_by, finishes, finished_by) are no longer keywords; Allen interval algebra ships as std::allen library vocabulary (RFD 0024), freeing those words for modelers. during remains reserved — it is the temporal-qualifier keyword (at | during | since), not an Allen operator.
Not reserved at lexer level — introducer names are ordinary identifiers: type, rel, and any vocabulary-introduced metatype or metarel name (UFO’s kind, subkind, role, phase, category, relator, mixin, mediation, material, BFO’s class, …). These are contextual identifiers resolved per name resolution against the pub metatype / pub metarel declarations visible in scope — names come from vocabulary packages, declared in the using package or imported from an external one; an unresolved introducer is OE0605 / OE0606. type and rel are not ambient: they are declared in std::core (see the standard library) and brought into scope by use std::core::{type, rel} or a [package].prelude entry (RFD 0038), exactly like any vocabulary. No vocabulary — not even the std::core baseline — ships ambient.
Compiler-known attribute names (reserved against macro shadowing, but not lexer keywords) live in a separate list — see annotations.
Out of scope (v0)
Deliberately deferred:
- Set/map literal syntax (
{1, 2, 3},{"a": 1}). - Registry and VCS dependencies.
ox buildresolves[dependencies]path dependencies only (Build pipeline); a bare version requirement (ufo = "1.0"), theversion/git/branch/tag/rev/registrykeys, and theox.locklockfile refuse withOE1240. - Federated
service <endpoint> { … }external-store calls. - Managed bridge rules — the
#[add]/#[delete]/#[revise]/#[override]operators (Brewka et al. 2011) that withdraw or rewrite target-standpoint conclusions, and the#[managed]escape from the bridge-graph acyclicity requirement (Bridge rules). - MLT* orderless types.
#[brave]/ stable-model semantics — multiple two-valued models with credulous and skeptical readings, as an alternative to the default well-founded semantics (derive).- User-defined operators.
- Async / await.
- Const generics.
- Higher-kinded types.
- Runtime trait objects (
dyn Trait). - Topology declarations inside
metarelbodies. - Refinement-driven width-selection on primitive types (current behavior: arbitrary-precision; explicit
i32etc. for fixed-width). - Raw-identifier escape for hard-reserved keywords (e.g., a Rust-style
r#prefix). Today, vocabulary authors may freely use contextual identifiers (type,rel,kind,role, …) because they are not lexer-level reserved — see reserved keywords. Hard-reserved keywords (derive,query,mutate,check,fn, …) cannot currently be used as identifiers. If demand surfaces from vocabulary authors, anr#escape is the obvious extension. - Hypergraph path traversal. v0
Path<NodeT, EdgeT>(ThePathtype) is over binary relations only; n-ary relations (pub rel Transaction(seller, buyer, item)) are queryable as predicates but not path-traversable. A principled hypergraph traversal semantics — specifying which endpoint the path enters and exits through — is deferred. - Temporal proof tags — the 4-slot Governatori-Rotolo schema $\pm\partial^{t_d}@{t_r}, l_{t_l}$ over the bitemporal substrate (Temporal substrate, Defeasibility).
- Alternative defeasibility strategy vocabularies — ambiguity propagation, stricter team-defeat variants, dynamic preferences, MKNF hybrid knowledge bases, rational closure / KLM typicality. These arrive as
use-imported strategy packages, not switches on the Governatori strategy (Defeasibility).
These may land in later language versions.
Diagnostic codes
The compiler crate oxc-diagnostics holds the authoritative diagnostic catalog
(generated.rs), regenerated from the [[diagnostic]] entries in
oxc-syntax/grammar.toml. This appendix is generated from the same entries
— it cannot drift from the catalog (CI enforces it via cargo xtask check-drift).
Where prose elsewhere in the book disagrees with a row here, this table wins.
A code is O[E|W]NNNN — an O-prefix (legacy Ontolog/Oxide; carries through
pending a v0.3 rename), a severity letter, and a four-digit number.
The catalog has 270 codes (262 OE + 8 OW), of which 231 are live (a real emitter exists) and 39 are reserved for planned later stages.
| Severity | Meaning |
|---|---|
OE | Error: blocks elaboration or load |
OW | Warning: surfaces, does not block |
The Status column distinguishes a live code (a real emitter exists in the
toolchain) from a reserved one (declared up-front for a planned later stage,
no emitter yet — audit dc-04). A reserved code never fires today; it is listed
so the number is claimed and the planned gate is documented. There are no
library-namespaced codes (mlt::, potency::, ufo::) in the catalog: a
theory package authors its constraints as its own rules/checks over the neutral
substrate, never as reserved compiler codes. The MLT reasoning codes that once
sat in 19xx are gone for exactly that reason; the 19xx codes that remain are
the cross-level instantiation-body field-coverage gates (§5.2), which are
substrate-level, not theory-specific.
Range allocation
The number’s leading digits group codes by subsystem. These are conventions for allocation, not a hard partition; ranges with no codes today are omitted.
| Range | Subsystem |
|---|---|
| 00xx–01xx | Parse / syntax; name resolution and imports |
| 02xx | Type / elaboration / predicate-atom validation; membership-write gates |
| 05xx | Stratification |
| 06xx | Meta calculus, refinement, comparison handling, trait conformance |
| 07xx | Modules, attributes, temporal/modal, forget |
| 08xx | Field-modifier gates, world-assumption temporal index, positional construction |
| 09xx | World-assumption conservativity and placement |
| 10xx | CWA evaluation, field intent |
| 11xx | Bridges |
| 12xx | Build composition + .oxbin validation (§16) |
| 13xx | Runtime contract + rule-evaluation gates (§17) |
| 19xx | Cross-level instantiation bodies |
| 24xx | Collection operators |
Parse and name resolution (00xx–01xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0001 | Error | Live | UnexpectedToken | Parser found a token it did not expect at the current position. |
| OE0002 | Error | Live | UnterminatedString | String literal missing its closing ". |
| OE0003 | Error | Reserved | InvalidNumberLiteral | Numeric literal could not be parsed. The INT/REAL literal grammar is §2.5. |
| OE0004 | Error | Live | InvalidEscapeSequence | Unrecognized escape sequence in a string or char literal. |
| OE0005 | Error | Live | UnterminatedBlockComment | Block comment /* … */ missing its closing delimiter. |
| OE0006 | Error | Live | NonAssociativeChain | Comparison (== != < <= > >=) and membership (in, not in) operators are non-associative (§6.6): a bare chain like a < b < c is refused at parse. Pre-refusal the parser folded chains left-associatively, so a < b < c became the Bool-vs-Int comparison (a < b) < c — type-mangled, and in a rule body silently never-true (every row dropped under a green check). Write the conjunction explicitly (a < b && b < c); explicit parentheses ((a < b) == c) are accepted as written. |
| OE0007 | Error | Live | QueryClauseNotYetImplemented | A query select-body uses a result-shaping clause — group by … having, `order by … asc |
| OE0008 | Error | Live | ModifierMisplaced | A declaration modifier appears on a form that does not admit it. abstract attaches to metatype declarations (every type the metatype introduces is abstract) and per-type to concept declarations (pub abstract type Vehicle { … }); fixed is metatype-level ONLY (pub fixed metatype kind = { … }; — classification-mutability is a property of the behavior bundle, not of one type). Any other placement — abstract struct, fixed before a concept declaration, a modifier on enum/rel/trait/rule forms, or a repeated modifier — is refused at parse rather than silently dropped: a swallowed modifier would let the program LOOK constrained while the substrate enforces nothing. |
| OE0009 | Error | Live | EnumPipeFormRemoved | An enum declaration uses the `= variant |
| OE0011 | Error | Live | MissingTerminator | A bodyless declaration is missing its terminating ;. A declaration is terminated by exactly one of: its body’s closing } (pub category Animal { … }), its cover’s closing } (pub kind Vehicle { Car, Truck }), or — when it carries neither — a required ; (pub category Top;). This is the same item-termination rule as Rust (struct Foo; needs the ;, struct Foo { … } does not). The terminator used to be optional: with no ; and no body the parser could not tell where the declaration ended, so a following #[…] attribute was silently re-attributed to the prior declaration and mis-expanded, surfacing a wrong, unrelated error far from the cause. Add the ; at the end of the unterminated declaration. |
| OE0012 | Error | Live | CoverPipeFormRemoved | A concept declaration uses the `= A |
| OE0101 | Error | Live | UnresolvedName | Bare identifier did not resolve to any in-scope declaration. |
| OE0102 | Error | Live | ReservedModuleName | std is a reserved top-level module name; the compiler embeds the stdlib at this namespace. User-declared modules named std would shadow stdlib imports and are refused. Rename the module. |
| OE0103 | Error | Live | UnresolvedUseImport | A use a::b::C; import does not resolve to any item in scope. Before this gate a broken use was accepted clean and the only error was a misleading downstream OE0605/OE0101 far from the cause; now the use itself is refused, with a did-you-mean suggestion over the names visible in the target namespace (a dependency package’s pub surface declared in [dependencies], a sibling module, an intra-package pkg::/self::/super:: path). A glob use pkg::*; brings in the target’s pub surface and is never a silent no-op; a glob whose prefix is itself unresolved is the same OE0103. |
| OE0104 | Error | Reserved | GlobImportUnsupported | Reserved (no emitter): a glob use path::*; whose shape is not yet supported. The supported glob — re-export-aware import of a module’s pub surface — resolves; this code is held so a future unsupported glob variant refuses feature-named rather than silently importing nothing. |
| OE0150 | Error | Live | RelationSubsumptionArityMismatch | Relation R1 <: R declaration where R1 and R have different parameter counts. |
| OE0151 | Error | Live | RelationSubsumptionEndpointVariance | Relation R1 <: R declaration where an endpoint position’s type in R1 does not specialize (<:) the corresponding position in R. |
| OE0152 | Error | Reserved | RelationSubsumptionCardinalityViolation | Relation R1 <: R declaration where a slot’s cardinality [c..d] does not refine the parent’s [a..b]. Refinement requires c >= a and (d <= b or b = *). |
| OE0153 | Error | Live | RelationSubsumptionMetarelMismatch | Relation R1 <: R declaration where meta(R1) != meta(R) (MVP rule — cross-metarel subsumption is deferred). |
| OE0154 | Error | Live | RelationSubsumptionCycle | Relation subsumption graph contains a cycle (R1 <: R2 <: ... <: R1) — the substrate requires an acyclic subsumption DAG. |
| OW0010 | Warning | Live | NonCanonicalClauseOrder | A concept declaration writes the : iof-instantiation clause before the <: specialization clause. Both orders parse and mean the same thing — the two clauses are independent (parents are an unordered set on each axis) — so this is a style WARNING, not an error: it never blocks ox check/ox build. The canonical spelling places <: before : (pub subkind Penguin <: FlyingAnimal : Vertebrate_Species { … }), mirroring the order arithmetic — specialization parents sit at the concept’s own order, instantiation parents one above. oxfmt will normalize the order for you once the CST-driven formatter lands; reorder the clauses to silence the warning in the meantime. |
Type, elaboration, predicate atoms, membership writes (02xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0201 | Error | Live | TypeMismatch | Expected one type but found a different one — expected X, found Y. |
| OE0202 | Error | Live | ArityMismatch | Function call has the wrong number of arguments. |
| OE0203 | Error | Live | NonExhaustiveMatch | match arms do not cover every constructor of the scrutinee type. |
| OE0204 | Error | Live | UnknownField | Field access x.f — in a derive/check/bridge rule body or a fn/mutate body — where the receiver’s established type, a struct or concept (pub type ..., or a vocabulary introducer) including inherited fields and any operand : T type-test narrowing in scope (a rule-body type-test or a comprehension where e : T), declares no field named f. In rule bodies, reported only on provable absence: f must be missing from every established concept of the receiver, since an Argon individual may be classified under several concepts (a field on any asserted concept is admitted). Evidence-gated: a base with no established concept (an unbound variable, or a qualified path to a declared type/enum such as Color.red) is skipped. |
| OE0207 | Error | Live | RequiredFieldMissing | insert T { … } omitted a field that T or one of its <: ancestors declares as required at construction (intent Required — not T? / Truth4Of<T> — with neither a default nor a from-navigation source). Emitted by the runtime construct path (oxc-runtime) as a loud refusal with nothing committed, rather than silently minting an incomplete individual. Distinct from OE1014 RequiredFieldUnasserted, the CWA evaluation-time case; the OE0207 number is a known wart pending the v0.3 renumber. |
| OE0208 | Error | Reserved | AmbiguousFieldFromMultipleParents | Reserved (designed — §5.2): a concept declaration (or a field access) names a field declared by more than one parent on the <: or : axis, with no qualifier to pick which. The substrate offers no automatic merge, override-by-position, or last-wins resolution — every collision is resolved explicitly by qualifying the field with its parent (bar.Vertebrate_Species::habitat). This entry reserves the code for the elaborator’s diamond-resolution gate. |
| OE0210 | Error | Reserved | IofInsertOnRigidType | Reserved (designed, not yet wired — §7.5): a dynamic insert iof(x, T) reclassifying an individual into a rigid type (rigidity::rigid — kind, subkind, category) rather than an anti-rigid one (role, phase, mixin). To create a new individual of a rigid type, use insert T { … }. The rigidity gate is not yet wired into the runtime iof path; this entry reserves the code for it. |
| OE0211 | Error | Live | IofInsertOnDefined | insert iof / delete iof on a defined (iff) concept (or one with a defined <:-ancestor), whose membership is derived from the refinement predicate, not asserted. Update the underlying state instead; the substrate auto-classifies. Emitted by the runtime mutate path (oxc-runtime), including the upward-closed ancestor case (IofInsertOnDefinedAncestor). |
| OE0212 | Error | Reserved | MetaArgUnbound | Reserved refusal for an unbound argument position of the type-position reflection intrinsics (meta/iof/specializes/extent). implements is EXEMPT by design: $implements is a finite catalog-closed relation, so both of its positions may be free — enumeration (free t: the implementing types; free tr: a type’s traits) is the intended use. The current engine answers the other intrinsics’ free positions relationally too, by enumerating their materialized $-relations, so this code has no emission site today; it is reserved for argument shapes a future evaluator cannot satisfy relationally. |
| OE0213 | Error | Live | IofInsertIntoBot | insert iof(x, Bot) (or delete iof(x, Bot)) targets Bot/⊥, the empty concept (lattice bottom), which by definition has no instances — the assertion is a contradiction and is refused at ox check / ox build. A sound empty-category refusal that follows from Bot’s definition alone, NOT from concept-vs-concept disjointness (which stays reserved, OE1904). The dual Top/⊤ is the lattice join — every individual is an instance of it — so a rule body Top(x) ranges over the whole domain. |
| OE0220 | Error | Live | FactReferencesUnknownPredicate | pub fact P(...) references a predicate name that is not declared as a concept or relation in the module. |
| OE0221 | Error | Live | FactArgArityMismatch | pub fact P(args) argument count doesn’t match the declared arity of P (1 for concepts; the relation’s parameter count for relations). |
| OE0222 | Error | Live | FactArgTypeMismatch | pub fact P(args) argument shape doesn’t match a declared parameter type (e.g., a literal in a slot that expects an individual). |
| OE0223 | Error | Live | RuleReferencesUnknownPredicate | A rule body or head (derive / check / bridge) references a predicate atom P(...) whose head name does not resolve to a declared concept, relation, derive/query/check head, or reflection intrinsic in scope (§7.3.1). Predicate vocabulary must be declared; OWA governs the truth value of instances of a declared predicate, not whether the predicate name itself exists. |
| OE0224 | Error | Live | PositionalConceptInsert | A positional insert C(...) in a mutate body where C resolves to a concept constructs nothing — parenthesized insert asserts a relation tuple, and the head is a concept, not a relation. Use the brace form insert C { field = value, ... } to construct an individual. Construction is brace-only; parens are reserved for relation-tuple assertions. |
| OE0225 | Error | Live | RuleAtomArgArityMismatch | A predicate atom P(args) in a derive / check / bridge rule body or head supplies the wrong number of arguments for the declared arity of P (1 for a concept-as-predicate; the relation’s parameter count for a relation; the head arity for a derived predicate). The non-ground analogue of OE0221 (pub fact), split by context per the OE0220→OE0223 precedent. |
| OE0226 | Error | Live | RuleAtomArgTypeMismatch | An argument to a predicate atom P(args) in a derive / check / bridge rule body or head has a type incompatible with P’s declared parameter type at that position (e.g., a logic variable bound elsewhere to String used in a slot declared Int, or a literal of the wrong primordial). Reported only when both the argument’s type and the parameter type are concretely known. The non-ground analogue of OE0222 (pub fact). |
| OE0227 | Error | Live | RuleValuePathUnresolved | A multi-segment path (a::b) in a rule body resolves to nothing under the unified value-position resolution order (bound rule variable, enum constant, axis value, type reference, declared individual — and, in predicate position, rule heads / concept extents / relations first). Before this gate the lowering silently turned an unresolved path into a VARIABLE named by the joined path — rigidity::anti_rigid then tripped OE1303 as an “unbound variable”, and a path in predicate-argument position bound everything and over-derived. Refused at ox check / ox build instead, naming the path and the namespaces searched. Also fires when a bare A::B rule atom resolves to a CONSTANT (a constant is a value, not a predicate; the path :: Ident metaEq sugar applies only when the left side is a bound term AND constant resolution declines the path), and when a qualified path matches several declarations (qualify further). Fix the spelling, declare the missing symbol, or qualify the reference. |
| OE0228 | Error | Live | RuleValueNameAmbiguous | A bare identifier in a rule-body value position is BOTH a rule variable (a head parameter, or a comprehension/quantifier/aggregate binder) AND resolves to a declared constant (a declared individual). Refused naming both candidates rather than silently picking one — the lesson of Rust’s bindings_with_variant_name lint, promoted to a refusal: silently reading it as a variable shadows the constant (the pre-S0 behavior that made knows(x, carol) over-derive); silently reading it as the constant changes the rule’s arity semantics. Rename the variable, or qualify the constant (module::name). A bare identifier that occurs ONLY in body positions and resolves to a declared constant IS that constant — no ambiguity, no error. |
| OE0229 | Error | Live | UnknownFieldWrite | A mutate-body write names a field the target concept does not declare: a constructor field (insert C { f: v }), an update … set { f = v } assignment, or an insert … into x.f target, where f is missing from C and from every ancestor in its <: chain. Reported only on provable absence — the concept and its full ancestor chain must be declared in the elaborating file; cross-module targets defer to runtime. Without the gate a typo’d field name persists under a property id no read path resolves — silent data loss. |
| OE0230 | Error | Live | InsertIntoNonCollection | insert <elem> into x.f targets a field whose declared type is not a collection ([T] / List<T> / Set<T>). insert into appends to collection fields; on a scalar field it would silently replace the committed scalar with a one-element list, corrupting the field’s type and detaching every refinement and derive that reads it. Use update … set { f = … } for scalar writes. |
| OE0231 | Error | Live | PropertyIdCollision | Two distinct Type::field pairs fold to the same property NameRef under the hash-derived property_id_for_field stand-in. Property ids are masked into the INT4 wire band [1, 2^31-1] (the high bit is reserved); the 31-bit space leaves room for collisions at workspace scale (birthday bound ≈ 2^15.5 field identities), and a collision is silent data aliasing — the two fields would share one storage column, the later declaration’s reverse-lookup entry shadowing the earlier. Refused at module load: the closed-set ConceptDecl walk that builds the resolution map is the injectivity oracle, so the collision converts to a build/load refusal naming both pairs instead of corrupting writes. Rename one field, or rebuild once the sequential interning table retires the hash stand-in entirely. |
| OE0232 | Error | Live | RelationEndpointMissing | A write places an individual into a typed slot — a relation endpoint or a concept-typed field — that no committed event introduces: a dangling reference. The ruling: such writes REFUSE missing individuals at write time; an individual EXISTS iff some committed event names it (an iof assertion OR a property assertion — full classification is not required, preserving §3.5 domain-conservative ghost-individual semantics). Fires on three surfaces. STATIC: a pub fact R(…, e, …) whose identifier endpoint e is in no concept-membership pub fact C(e) in the elaborating file (build-time, all information in source; cross-module endpoints defer to runtime). RUNTIME (relation): an insert R(…) in a mutate body (or CLI/serve write) whose individual endpoint resolves to an id no iof/property assertion in the store names. RUNTIME (field): an insert C { f: o } / update t set { f = o } whose concept-typed field f is given an individual o that no committed event names — the field-side counterpart of the relation endpoint floor, since a concept-typed field is a typed slot exactly as an endpoint is. Both runtime surfaces are checked against the overlay’s read-your-writes view, so an individual constructed or classified earlier in the SAME body counts. Without the gate, a relation’s declared signature (or a field’s declared type) is decorative: derives joining serve rows whose endpoints are in no extent, and a forged below-floor #i reference would slip past the serve floor-bound. Atomic — raised before the tuple/field is buffered, so the whole body commits nothing. Existence is the FIRST gate; TYPING is the layer above it (OE0258): once the individual exists, a closed-world target concept refuses a write over an UNclassified individual (CWA cannot silently expand a closed extent), while an open-world concept admits it. delete iof-driven cascade is a recorded non-decision (deferred). Construct or classify the individual first, or fix the reference. |
| OE0233 | Error | Live | AbstractTypeConstruct | The program asserts a direct instance of an abstract type: insert T { … } where T is declared abstract (or is introduced by an abstract metatype), a direct insert iof(x, T) classifying an existing individual under it, or a positive pub fact T(x) seeding one at elaboration. abstract means NO DIRECT INSTANCES — abstract types structure the hierarchy (and are exempt from trait impl-coverage obligations, OE1327) precisely because every individual is classified through some non-abstract subtype. Assert or classify under a non-abstract subtype instead; subtypes are unaffected, and pub not_fact T(x) (refuting membership) stays legal. Emitted at ox check/ox build where the target is statically known; the runtime enforces the same refusal at the write path (atomically — the whole mutation rejects) for targets only resolvable at execution, and at artifact load for hand-built IofAssertion events that would bypass the elaborator. |
| OE0234 | Error | Live | FixedReclassification | A mutate body re-classifies under a type introduced by a fixed metatype: insert iof(x, T) or delete iof(x, T) where T’s introducing metatype is declared fixed. fixed means classification is DECIDED AT CONSTRUCTION — membership of fixed-introduced types never changes over an individual’s lifetime, which is what makes static check discharge over them sound (§10.2: rigid designation = fixed classification). Construction (insert T { … }) is NOT re-classification and is unaffected; types introduced by non-fixed metatypes classify and de-classify freely (dynamic is the default — the restriction is the opt-in, the same posture as check). Emitted at ox check/ox build where the target is statically known; the runtime write path enforces the same refusal (atomically) otherwise. Importing a vocabulary can never change mutation semantics by axis NAME — only the declared fixed modifier carries this behavior. |
| OE0235 | Error | Live | GenericTypeApplicationUnsupported | A generic type application carries arguments the elaborator does not interpret. User-declared concept generics are decorative: a concept’s <T> parameters parse but reach no storage slot (ConceptDeclBody has no type-parameter column), so an application like Boxy<Int> / Boxy<Int, String, Bool> / a bare Boxy (arity 0 against a 1-param concept) / NotGeneric<Int> (arguments on a non-generic concept) was accepted with the arity and applicability silently discarded — the §6.2 ‘generic applications subtype only by equality’ rule and the §6.1 bounded-generic grammar both imply the arguments are semantically real. The library generics with a real construction surface are unaffected (List<T>, Set<T>, Option<T>, Result<T, E>, Range<T>, Truth4Of<T>, and the live reflective TypeRef<C>); their argument shapes ARE interpreted. Refused at ox check / ox build at the use site until parameterised user concepts are real (the type-parameter plane is V1 bounded-generics territory). Drop the arguments, or use one of the supported library generics. |
| OE0236 | Error | Live | MutateFieldValueTypeMismatch | A value position in a mutate body is assigned to a declared field whose type it does not match: an update target set { f = v } whose v is a value of a primordial incompatible with f’s declared type (a String into an Int field, an Int into a Date field, a structured CBOR record into a scalar field). Pre-refusal these assignments bypassed any runtime re-check (the write path’s coerce_value_for_declared_type was temporal-coercion-only, falling through other => Ok(other) for the mismatch), so the wrong-typed value committed and downstream rules derived silently-wrong rows (an Int in a Date field made d < <date> unconditionally true under the cross-type comparison fallback — see OE0630). Emitted at ox check / ox build for the statically-decidable, sound subset — a primitive literal whose primordial type UNCONDITIONALLY contradicts the field’s declared primordial type (the constructor insert C { f: lit } and update target: T set { f = lit } paths), refused before a doomed artifact is built rather than late at query/serve. The static gate is a STRICT SUBSET of the runtime gate: value-dependent pairs the runtime may coerce (a String literal into a parse-coercible temporal / exact-numeric field, an Int into the numeric tower or Duration day-count, an integral Real into Int/Nat) stay deferred to the runtime mutate value path, which remains the backstop for those and for cross-module field-type chains. The Value::Cbor carrier seam is bounded by tag inspection: a tag-30 exact rational is checked against the numeric tower (accepted into Real/Decimal/Money/Int, refused into Date/Duration/String/Bool), and a structured map/array/enum-payload carrier is refused into the NATIVE primordials that never ride a structured carrier (Int/Nat/Real/Date/Duration/String/Bool) — a Money struct into Int is the canonical refusal. Money and Decimal are EXEMPT: today they are represented AS opaque/structured CBOR carriers, so a structured value is their legitimate form; an indecodable opaque blob or a concept-typed field also passes through. The field-coercion path (Text→Date/Duration/Real parse, Int→Duration day-count) stays accepted where the surface defines it. |
| OE0237 | Error | Live | FieldDefaultUnsupported | A field declaration carries a default-value clause field: T = expr. Book §5.1 specifies full default semantics (lazy evaluation at access, self resolution, resolution order), but the elaborator never reads the expression — the parser consumed it as token soup and the lowering hardcoded default: None, so the default silently never applied: a required field with a dropped default still refused construction (OE0207), and an optional field T? = expr constructed successfully with the default absent (silently wrong extents downstream). Refused at parse until the lazy-default evaluator lands rather than accepting a clause the substrate ignores. Drop the = expr and supply the value at construction, or use a from-navigation source (field: T from Rel.endpoint) for a derived value. The from-clause form is unaffected — it is parsed and elaborated. |
| OE0238 | Error | Live | UpdateOnUnclassified | update … set { … } targeted an individual that is an instance of no concept at all — its classification set is empty. A primitive (where) concept’s necessary invariant is keyed on a classifier, so an unclassified target evaluates none of them: the field write proceeds wholly unchecked and the corrupt value silently resurfaces (bypassing the invariant) if the individual is later classified into a concept the value violates. Refused loudly by the runtime mutate path (oxc-runtime) rather than written — classify the individual first (insert iof(x, T) or construct it with insert T { … }), then update. Atomic: nothing flushed. Scoped fix; re-validating all properties at classification time is a separate follow-up. |
| OE0239 | Error | Live | FactReferencesDerivedPredicate | pub fact P(...) names P, a predicate defined by a pub derive / pub query rule (an intensional / IDB head), not a base predicate. pub fact asserts into an EXTENSIONAL relation (a concept-as-classification or a pub rel); a derived predicate’s extent is COMPUTED by rule derivation, and a pub fact seed would key a different relation node than the rule head reads, so it silently DROPS OUT of the fixpoint (no error, wrong answer). Refused at ox check / ox build rather than emitting an artifact that mis-derives. To give a derived predicate ground tuples that participate in its derivation, use one of the two sanctioned forms: (1) a bodiless pub derive P(args); clause — a derive head with no :- ... body over CONCRETE arguments is itself a ground fact that unions with P’s other derive clauses and rules over the same head; or (2) declare a base relation the rule reads — pub rel Base(...), seed it pub fact Base(...), and add a pub derive P(...) :- Base(...) clause. Either keeps P a single-origin intensional head; pick the bodiless-derive form to seed P directly, the base-rel form when the seed tuples are themselves a reusable extensional predicate. |
| OE0240 | Error | Live | GroupAxiomDisjointOverlap | A disjoint { A, B, … } group axiom declares its members pairwise non-overlapping, but a write would classify one individual into two of them at once (iof(x, Ai) and iof(x, Aj)). The membership is refused at the write rather than admitted, since an individual in two declared-disjoint siblings makes the partition contradictory and every downstream query over either sibling silently wrong. Carried by a compiler-synthesized check (one per unordered member pair) whose runtime delta-guard refuses the offending insert. Instance-level under the closed-world default (§6.9). #[world(open)] is a live concept attribute, but this disjoint delta-guard evaluates closed-world regardless of the concept’s world — the open-world tolerate softening is designed and not yet wired (the runtime guard does not read the per-concept world map). Retract the conflicting membership, or remove the member from the disjoint set if the overlap is intended. |
| OE0241 | Error | Live | GroupAxiomUncovered | A complete Parent { A, B, … } (or partition) group axiom declares its members jointly exhaustive over Parent, but a write would create a Parent instance that is a member of none of them (iof(x, Parent) with not iof(x, Ai) for every cover member). Under the closed-world default (§6.9) the negation-as-failure covering guard fires when no child membership is derivable, and the write is refused rather than admitting an uncovered Parent instance the cover claims cannot exist. Carried by a compiler-synthesized check whose runtime delta-guard refuses the offending insert. #[world(open)] is a live concept attribute, but this covering delta-guard evaluates closed-world regardless of the concept’s world — the open-world tolerate (permit-as-unknown) softening is designed and not yet wired (the runtime guard does not read the per-concept world map). Classify the instance into one of the cover members, or widen the cover. |
| OE0242 | Error | Live | GroupAxiomUncoveredSubtype | A complete Parent { A, B, … } (or partition) group axiom declares its members jointly exhaustive over Parent, but the schema declares a NON-abstract concept S <: Parent (transitively) that specializes none of the cover members — a provably-uncovered instantiable class. Any instance of S is a Parent instance in no cover member, so the cover is statically incomplete. Refused at build by a direct elaboration pass over the <: graph (not a reasoner check — instantiable has no catalog atom to range over). An ABSTRACT subtype is exempt: it has no direct instances, so it cannot witness an uncovered instance. Add S (or one of its supertypes inside the cover) to the cover, mark S abstract, or interpose a cover member S <: Ai. |
| OE0243 | Error | Live | GroupAxiomNonSubtypeChild | A complete Parent { …, S, … } (or partition) group axiom names a cover member S that is not a subtype (<:, transitively) of the declared Parent. A cover’s members partition the parent’s extent, so a non-subtype member could classify individuals outside Parent — the covering / disjointness guards would then range over the wrong extent. Refused at supertype resolution. disjoint { … } has no parent and imposes no such constraint. Declare S <: Parent, or remove S from the cover. |
| OE0244 | Error | Live | FromClauseUnknownRelation | A navigation-view projection field (field: T from Rel.endpoint) names a relation Rel that does not resolve to a declared pub rel in scope. The from clause projects the field’s value from one endpoint of that relation, so a relation that is not in scope leaves the field projecting nothing — and the clause was previously never checked: only the runtime read from_relation, so a bogus relation built clean and silently projected an empty view. The relation path is now resolved at ox check (the same name-resolution gate concept/relation references pass), and an unresolved or non-relation head is refused. Declare the relation, import it (use), or correct the path. |
| OE0245 | Error | Live | FromClauseUnknownEndpoint | A navigation-view projection field (field: T from Rel.endpoint) names an endpoint accessor endpoint that is not one of the relation Rel’s declared endpoint names (nor the positional domain/range aliases for the first/last endpoint of a binary relation). The endpoint selects which relation position the field projects; an accessor that matches no position projects nothing — and was previously unchecked (only the runtime read it, so a bogus endpoint built clean and silently projected an empty view). The accessor is now validated against the relation’s declared positions at ox check. Use one of Rel’s declared endpoint names, or domain/range. |
| OE0246 | Error | Live | FromClauseEndpointTypeMismatch | A navigation-view projection field (field: T from Rel.endpoint) declares a field type T whose concept is <:-incomparable with the type E of the relation endpoint it projects — neither is a subtype of the other. A navigation view is type-directed: it projects the endpoint and keeps the members that are iof T, so the view is sound for any <:-comparable (T, E) pair (identity when T == E, a no-op widening when E <: T, a real iof T filter when T <: E). But when T and E are unrelated, no endpoint value can be iof T: the view is statically empty and the declaration is type-incoherent. (The narrowing case T <: E — e.g. circles: [Circle] from Contains.shape over a Shape endpoint — is now ACCEPTED and filters to the T-typed members; it no longer errors here.) Widen the field type to a supertype of E, or project an endpoint whose type is <:-comparable with T. |
| OE0247 | Error | Live | FromClauseFilterContradiction | A navigation-view projection field (field: T from Rel.endpoint where <predicate>) carries an explicit where type-test endpoint : C whose concept C is <:-incomparable with the field’s element type T. The type-directed view already keeps only the iof T members; a where endpoint : C restates or narrows that selection and must be CONSISTENT with it. When C and T are unrelated, the conjunction iof T && iof C is unsatisfiable: the view is statically empty. A consistent restatement names T itself (where shape : Circle on a [Circle] field) or a <:-related concept; a value predicate (where shape.radius > 0) imposes no such constraint. Drop the contradictory type-test, or align it with the field type. |
| OE0248 | Error | Live | FieldValueNotIndividual | A write to a concept-typed field supplies a primitive value where an individual is required: a constructor field (insert C { f: <literal> }) or an update … set { f = <literal> } assignment whose declared field type f: D is a declared CONCEPT, given a primitive literal (a string / integer / decimal / boolean). The value-vs-individual mismatch is the one HARD-CATEGORY refusal sound under Argon’s open-world, multiply-classified semantics (the same arm as OE0222 for pub fact relation endpoints): a value is never an individual of any concept, so it can never inhabit a concept-typed slot — storing it would key a property the read path resolves to a non-individual, silently violating the declared shape. The symmetric arm (an individual written to a primitive-typed field) is also refused. This is NOT a concept-vs-concept disjointness check: an individual of any concept supplied to a concept-typed field is ACCEPTED (it may be reclassifiable; two concepts are never provably disjoint without a declared partition). Supply an individual reference (a constructed binding, a parameter, or a declared individual) for the concept-typed field, or give the field a primitive type if a value was intended. |
| OE0249 | Error | Live | StructFieldMissing | A struct VALUE literal T { … } omits a field that T declares: a struct is pure data with structural equality (§5.1), so a literal states every field — there is no construction-time default (field: U = expr defaults are a separate, refused surface). The omitted field’s name and declared type are named. Add the missing field: value, or, when constructing from an existing value of the same struct, use functional update T { ..base, field: value } so the base supplies the rest. |
| OE0250 | Error | Live | StructFieldExtra | A struct VALUE literal T { … } supplies a field that T does not declare. A struct is closed data (§5.1): only declared fields may be initialized, and an unknown field is a typo or a stale field name rather than an extension point. Remove the field, or correct it to one of T’s declared field names. |
| OE0251 | Error | Live | StructFieldTypeMismatch | A struct VALUE literal T { field: value } initializes a field with a value whose type does not match the field’s declared type. A struct is pure data with structural equality (§5.1); storing a mistyped field would make two values compare unequal-by-type or derive a silently-wrong projection, so the mismatch is refused at build time, naming the field, its declared type, and the supplied type. Supply a value of the declared field type. |
| OE0252 | Error | Live | InsertOfStruct | insert T { … } names a struct, but insert constructs a concept INDIVIDUAL — it mints an identity and records an iof classification. A struct is pure data with no metatype and no identity (§5.1): it is constructed as a plain VALUE T { … } (e.g. a let binding, a field value, an argument), never inserted. Drop the insert keyword to construct the struct value, or name a concept type if an individual was intended. |
| OE0253 | Error | Live | MutOnStructField | A struct field is declared mut. A struct is an IMMUTABLE pure value (§5.1): its fields are fixed at construction and updated only by functional update T { ..base, field: new }, which yields a NEW value rather than mutating in place. The mut modifier marks a concept field updatable by an update … set { … } statement — a notion that applies to identity-bearing concept individuals, not to struct values. Remove mut from the struct field. |
| OE0254 | Error | Live | StructFieldAccessUnknown | Field access v.f where v is a struct VALUE whose declared struct type has no field f (§5.1). A struct projects field-wise off its declared fields; an absent field is a typo or a stale name. Use one of the struct’s declared field names. |
| OE0255 | Error | Live | StructValueIntoNonStructField | A struct VALUE is supplied where a field of a different type is declared: a struct value S { … } written to a field whose declared type is a primitive, a concept, or a different struct/enum type. A struct value has structural equality and no identity (§5.1), so it inhabits only a field declared with its own struct type — storing it into an incompatible field would key a value the read path cannot interpret. Supply a value of the declared field type, or change the field’s type to the struct type if the struct value was intended. |
| OE0256 | Error | Live | EnumPayloadArity | An enum payload-variant construction Variant(args) supplies a number of arguments other than one. A payload variant carries a single value (§5.1) — the tag-1011 value carrier holds one payload slot — so Some(7) is well-formed while Some() (zero) and Some(1, 2) (two) are not; the surplus or missing argument is refused rather than silently dropped or truncated. Supply exactly one payload value, or construct a payloadless variant (None) with no parentheses. |
| OE0257 | Error | Live | EnumPayloadPattern | A payload-binding match arm Variant(binder) does not bind exactly one fresh variable: a zero-binder Variant(), a multi-binder Variant(a, b), a nested non-binding sub-pattern, or the record form Variant { … }. A payload variant carries one value (§5.1), so its destructuring binds exactly one fresh variable positionally — write Variant(x) => …. Rewrite the arm with a single binder, or match the variant without binding by hoisting the test. |
| OE0258 | Error | Live | RelationEndpointTypeClosed | A write places an individual into a typed slot — a relation endpoint (insert R(…)) or a concept-typed field (insert C { f: o } / update t set { f = o }) — that EXISTS (OE0232 passed) but is classified under NOTHING — a bare/ghost individual known only by a property assertion — while the slot’s declared concept T is governed by the CLOSED-world assumption (§6.9). Under CWA concept membership is closed (absence of evidence is evidence of absence), so admitting the write would silently mint the first membership claim about the individual and expand T’s closed extent — refused, the typing layer above OE0232’s existence-only gate. The world assumption DECIDES this arm and nothing else: an OPEN-world T admits the same write (membership assumed-as-unknown), with no diagnostic. Membership is the individual’s full classification (asserted iof UNION derived iff, <:-closed — the decision query_extent/$meta/dispatch share), read against the overlay’s read-your-writes view, so classifying the individual earlier in the SAME body satisfies the gate. Only genuine CONCEPT slots are checked — a primordial position (rel R(a: String, …)) or a primitive field has no extent and is skipped (a primitive into a concept field is the distinct OE0248 value-vs-individual refusal). The Reserved boundary (OE1904) demarcates this gate precisely: when the individual IS classified, but under a concept disjoint from T only by intent (the reversed case — o is iof Pet, the slot wants Person), the write is ACCEPTED, because two concepts are never provably disjoint without a declared partition. Atomic — raised before the write is buffered, so the whole body commits nothing. Classify the individual under {concept} first (this same mutate body counts), or declare {concept} open-world if its membership should be assumable from a write. |
Stratification (05xx)
Reserved range — the stratifier’s pre-runtime aggregate diagnoses. The runtime equivalents fire as OE13xx rule-compile gates today.
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0510 | Error | Reserved | NonStratifiedAggregate | Reserved (designed, not yet emitted as OE0510 — §7.2): a stratified aggregate that would loop through an aggregation step on its own stratum (no well-defined fixpoint). The runtime today refuses recursion-through-aggregation as OE1317 RecursionThroughAggregation at the rule-compile gate; this 05xx code is reserved for the stratifier’s pre-runtime diagnosis of the same shape. |
Meta calculus, refinement, comparison, traits (06xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0605 | Error | Live | UnknownMetatype | A concept declaration’s introducer (the identifier before the concept name — pub <introducer> Name) does not resolve to a pub metatype visible in scope. Introducers are not keywords: per §3.4 the leading identifier resolves against pub metatype declarations — declared in this package or brought into scope by a use/the package prelude (§5.2). Nothing is ambient: even the baseline type (and rel for relations) must be in scope — add use std::core::{type, rel}; or list prelude = ["std::core::{type, rel}"] under [package]. Two fixes: bring the baseline into scope and use the ontology-uncommitted pub type Person;, or declare/import the vocabulary the introducer belongs to (pub metatype kind = { }; makes pub kind Person legal). Vocabulary classifiers (UFO’s kind, BFO’s class, …) are package vocabulary, not language surface — std::core’s own docs: type is the no-commitment baseline; vocabulary packages ship alternative metatypes. |
| OE0606 | Error | Live | UnknownMetarel | A relation declaration’s introducer (pub <introducer> Name(...)) does not resolve to a pub metarel visible in scope. Per §3.4 the leading identifier resolves against pub metarel declarations — declared in this package or brought into scope by a use/the package prelude (§5.2). Nothing is ambient: even the baseline rel must be in scope — add use std::core::{type, rel}; or list prelude = ["std::core::{type, rel}"] under [package]. Two fixes: bring the baseline into scope and use the neutral pub rel Owns(owner: Person, thing: Asset);, or declare/import the vocabulary the introducer belongs to (pub metarel mediation<E1, E2>(e1: E1, e2: E2); makes pub mediation Involves(...) legal). Relation-vocabulary classifiers (UFO’s mediation, material, …) are package vocabulary, not language surface. |
| OE0612 | Error | Reserved | OptionComparisonRequiresHandling | Rule body contains Option<T> comp_op T (or comp_op Option<T>) without explicit None-handling. Use is Some(a), a comp_op ... / `.is_some_and( |
| OE0613 | Error | Reserved | Truth4ComparisonRequiresHandling | Rule body contains Truth4Of<T> comp_op T without explicit four-valued handling. Use is Is(a), a comp_op ... / is Not / is Can outcome suffixes, or match { Is(a) => ..., Not => ..., Can => ..., Both => ... }. |
| OE0620 | Error | Live | AxisValueNotInAxisDomain | A rule-body value position names an axis value (rigidity::anti_rigid — a path whose first segment resolves to a visible pub metaxis declaration) that the axis’s declared domain does not admit: either the value is not a member of the enumerated domain (the diagnostic lists the declared values), or the axis declares a TYPED domain (pub metaxis weight for metatype = Real;), whose values are literals with no enumerated-symbol spelling. A valid enumerated axis value never reaches this code — it lowers to a Value::Symbol constant (owner = the axis identity, ord = the chain position) and evaluates against the catalog-closed $axis relation. (Pre-S3 this code refused EVERY axis value as not-yet-evaluable; that scope is retired — the rule-body diagnostic that remains is exactly the not-in-domain / typed-domain-spelling case.) |
| OE0621 | Error | Live | UnknownMetaxis | A metatype / metarel body binds an axis (axis: value) whose axis name does not resolve to a visible pub metaxis declaration — or matches several, in which case every candidate is named and a qualified spelling (vocab::rigidity: rigid) disambiguates. Axes are not ambient vocabulary: declare the axis in this package (pub metaxis rigidity for metatype { anti_rigid < semi_rigid < rigid };) or import the vocabulary package that declares it. Pre-S1 these bindings were parsed and silently DROPPED; now every binding resolves to the declaring metaxis’s identity and is persisted on the wire, so an unresolvable axis is a hard refusal, never a phantom. |
| OE0622 | Error | Live | AxisValueNotInDomain | An axis binding’s value is not a member of the axis’s declared domain. For enumerated domains ({ a, b } unordered or { a < b < c } chain) the axis: value binding must name one of the declared values — the diagnostic lists them. Binding an enumerated axis with a literal (axis: 1.5), or a typed axis with a bare symbol (axis: rigid), is the same refusal: the value shape follows the domain (a bare symbol for enumerated domains, a literal for typed ones). |
| OE0623 | Error | Live | AxisLiteralTypeMismatch | A typed-domain axis binding’s literal does not satisfy the axis’s declared TypeExpr (or statically fails its where { … } refinement) — e.g. weight = "heavy" against pub metaxis weight for metatype = Real where { _ > 0.0 };. Checked statically for the primitive domains (Int / Nat / Real / Decimal / String / Bool); a refinement comparing the placeholder _ against a literal is also discharged statically. Domains the elaborator cannot decide statically are not refused here (they validate when the $axis relation materializes, slice S3). |
| OE0624 | Error | Live | AxisTierMismatch | An axis is bound on a declaration tier its for targets do not include: a pub metaxis … for metatype axis binds only inside metatype bodies, for metarel only inside metarel bodies. Axes declared for individual / for macro currently bind NOWHERE — those binding surfaces do not exist yet, so a metatype/metarel binding against such an axis is refused with this code rather than silently accepted against a tier that can never consume it. Fix: bind an axis whose targets include this declaration’s tier, or extend the axis’s target list (pub metaxis a for [metatype, metarel] { … };). |
| OE0625 | Error | Live | DuplicateAxisBinding | A metatype / metarel body binds the same axis twice (e.g. { rigidity: rigid, rigidity: anti_rigid }) — malformed per the §4 distinct-axes well-formedness constraint (Argon.MetaCalculus.Wellformed.MetatypeDecl.axesDistinct). Axis assignment is functional per (declaration, axis): a second binding could only restate or contradict the first, so both occurrences are named and the declaration is refused. Two spellings of one axis (bare rigidity and qualified vocab::rigidity) count as duplicates — identity is the RESOLVED axis. |
| OE0626 | Error | Live | MetaxisDeclMalformed | A pub metaxis declaration’s shape is malformed: an enumerated body mixes , and < separators (a domain is either unordered { a, b } or a chain { a < b < c }, never both); the body is empty or absent (an axis with no domain admits no value, so every binding against it would be refused — declare the values or the typed domain); a value is repeated; the for target list is empty or names something other than metatype / metarel / individual / macro. The declaration is refused at ox check / ox build so the wire never carries a domain the validator cannot interpret. |
| OE0627 | Error | Live | SymbolComparisonUnordered | An ordered comparison (< <= > >=) over symbol values that carry no common chain order: the operands belong to DIFFERENT owners (different metaxes / different enums — chain order is per-owner), the owner declares an UNORDERED domain ({ a, b } — declare a chain { a < b < c } to compare by order), or a symbol is ordered against a non-symbol value. Emitted at ox check / ox build when both operands are statically known (axis-value / enum constants, or an axis projection whose axis identity is known); the reasoner and the mutate path refuse the same comparison at runtime under the same code — never the enum-variant-order fallback, never a silent row drop. Equality (== / !=) stays defined everywhere: symbols are equal exactly on (owner, name) identity. |
| OE0628 | Error | Live | ReflectiveProjectionSortAmbiguous | A field projection base.name the sort-directed reading cannot dispatch soundly: (a) name resolves to a visible pub metaxis but the SORT of base is statically UNDETERMINED (or the body pins it to two contradictory sorts) — a Metatype-sorted base reads the metaxis through the $axis relation (meta(t).rigidity with t: TypeRef), while a TypeRef- or individual-sorted base reads a declared FIELD named name (the §5.2 kind-field walk-up); two different relations, so guessing would silently answer the wrong question; fix it by making the base’s sort explicit — annotate the head parameter (t: TypeRef / m: Metatype), or bind the base with a sort-determining catalog atom (m : Metatype, iof(t, kind), an equality with a sorted variable — Var == Var equalities propagate sorts). (b) the base IS Metatype-sorted but no visible metaxis is named name — metatypes carry axis bindings, not fields, so the $field:: join would be silently empty; declare/import the metaxis, or project the field from a type or individual. A projected name matching SEVERAL visible metaxes on a Metatype-sorted base is refused through the axis resolver’s ambiguity arm (OE0621). |
| OE0629 | Error | Live | AxisAssignmentConflict | Two same-precedence $axis assignments disagree for one (target, axis) pair at module load (the effective axis relation is functional — a metatype binding may be overridden by a per-target meta_property event, but two conflicting assignments at the SAME precedence level have no defined winner, so the module refuses to load rather than silently pick one). No surface form double-asserts today (duplicate axis bindings refuse at OE0625; duplicate #[order] refuses at elaboration); this code is the artifact-trust backstop for hand-built or corrupted event logs. Emitted by oxc-runtime at load; declared here so appendix C and ox diagnostics carry it. |
| OE0630 | Error | Live | IncomparableValueTypes | An ordered comparison (< <= > >=) is evaluated between two values whose types carry no common order: e.g. an Int against a Date, a number against a Text. Pre-refusal the reasoner’s value_order fell back to the derived Ord of the Value enum — comparing by variant discriminant — so a mistyped field (an Int stored in a Date field, OE0236’s runtime cousin) made field < <date> unconditionally true and field > <date> unconditionally false regardless of the actual values, silently deriving wrong rows. Now a loud ReasoningError, parallel to the existing temporal-mismatch (TemporalTypeError) and unordered-symbol (OE0627) refusals, raised in both the rule evaluator and the mutate value path — never the enum-variant-order fallback, never a silent row drop. Equality (== / !=) stays defined across types (distinct types are simply unequal) and never routes through the ordering refusal — only the four ordering operators can raise OE0630. Numeric ordering across the exact-numeric tower is defined for the Int/Real carriers (and Decimal/Money operands that ride a Value::Real/literal at evaluation): both promote to a common BigRational and compare by value. A Money/Decimal value stored as an opaque Value::Cbor carrier does NOT decode to a rational at the ordering site — it byte-compares as an unordered structured carrier and is refused (OE0630), not silently mis-ordered; ordering stored exact-decimal carriers is the recorded follow-on, not a v0 guarantee. |
| OE0631 | Error | Live | MetarelEndpointMetatypeMismatch | A relation declared via a metarel introducer (pub mediation Involves(m: Marriage, p: Person)) has an endpoint whose metatype does not match — and is not a sub-metatype of — the metatype the classifying metarel declares at that position (§4.3). A pub metarel mediation(mediator: relator, mediated: kind) requires position 0 to be relator-sorted and position 1 kind-sorted; an endpoint introduced under any other metatype (or a non-sub-metatype) is refused. The §4.3 promise — “the elaborator verifies the relation’s endpoint metatypes match the metarel’s positions” — was previously prose only: the position metatype names were parsed and discarded, so a wrong-sorted endpoint (Gustavo’s inheresIn(v: Vehicle, …) where an ability must inhere via an aspect, not a kind) checked clean. The check is catalog-driven and axis-name-agnostic — it compares resolved metatype identities, never a metatype/axis name string. A metarel position naming a primordial or a generic parameter (the stdlib rel<E1: metatype, E2: metatype>) imposes NO constraint; the gate fires only when both the position metatype and the endpoint metatype are known and incompatible. |
| OE0632 | Error | Live | ReflectionOnUnclassified | A reflection intrinsic (meta / iof / specializes / extent) is applied to a struct- or enum-declared carrier — language-level DATA, not an ontologically-classified concept (§4.4.2). Language data carries no metatype, so meta(p) == Point over a pub struct Point is a category error, parallel to OE1016 (Truth4OfOnStruct) for the Truth4 surface. The discipline §4.4.2 leans on (“calling meta() on a struct/enum-declared value is a category error … emits a diagnostic at elaboration”) was previously unenforced — the call checked clean and a vocabulary author writing nonsensical reflection over plain data got zero feedback. Reflection applies only to concepts introduced under a vocabulary or stdlib metatype; classify the carrier under a metatype, or drop the reflection. |
| OE0633 | Error | Live | MetarelEndpointArityMismatch | A relation declared via a metarel introducer (pub mediation Involves(m: Marriage, p: Person)) has a different number of endpoints than its classifying metarel declares positions (§4.3). A pub metarel mediation(mediator: relator, mediated: kind) declares two positions, so a relation introduced under mediation must have exactly two endpoints — a ternary mediation R(a, b, c) and a unary mediation R(a) are both refused. The §4.3 endpoint-verification step zipped the relation’s endpoints against the metarel’s positions and silently continued past any position beyond the metarel’s arity, so an over- or under-arity relation checked clean (its surplus or missing endpoints were never validated). The guard is arity-EQUALITY against the metarel’s declared endpoint count — the generic stdlib rel<E1: metatype, E2: metatype> declares two positions and still accepts exactly its two endpoints; a metarel whose positions are all unconstrained (None) still requires the matching count. It is not relaxed to “accept anything”: a metarel imposing no per-position metatype constraint still pins the arity. Forward-compatible — only relaxes if metarel genericity (variadic positions) later lands. Mirrors arityEqual (Argon.Substrate.RelationSubsumption) and the OE0150 relation-arity precedent. |
| OE0634 | Error | Live | AxisSetFormMismatch | A metaxis binding uses the wrong form for its domain’s arity. A set-valued domain (pub metaxis restrictedTo for metatype = [ Nature ]) binds ONLY with the set form axis: [a, b]; a scalar domain (enumerated { … } or single Typed) binds ONLY with a single axis: value. Crossing the two — a scalar value against a set domain, or […] against a scalar domain — is refused here. This consolidates what were divergent, non-agreeing refusals for one need: a set binding against a scalar domain previously tripped OE0626 (“expects a value, found [”), and a single value against a set domain tripped OE0622 — separate codes for “you used the wrong binding shape.” OE0634 is the single FORM gate; a set MEMBER that fails the element type / refinement is OE0623 (the per-element type check), not this. Emitted at elaboration and re-checked at artifact load (a crossed form reaching the runtime is a wire inconsistency). The set form is admitted only by a set-domain axis (AxisDomainBody.admits — Argon.Storage.AxisBindingValid). |
| OE0660 | Error | Live | RefinementUnsupportedForm | A concept’s where { … } refinement predicate contains an expression form the v0.1 evaluator doesn’t support. Supported forms: literals, self/identifiers, field projection self.field, binary ops (arithmetic / comparison / && / ` |
| OE0667 | Error | Live | TraitImplBoundsNotSupported | Generic-parameter trait bounds on pub trait or impl blocks are reserved syntax but not yet implemented in v0.1. The conditional-impl coherence theorem (Argon.TypeSystem.Conditional) admits multi-impl resolution under disjoint bounds; the elaborator side ships in V1. Use pub trait T<U> / impl<U> Type (without bounds) for now. |
| OE0668 | Error | Live | RefinementInvariantViolated | A membership write would place an individual in a primitive (where) concept whose necessary invariant it positively violates — the predicate evaluates to a definite false. unknown permits the write (information absence, not violation). Checked when a primitive-where member is created by insert iof, by construction (insert T { … }), or when an update writes a field the predicate reads. Emitted by the runtime mutate path (oxc-runtime); the same exact value tower as every other value position. |
| OE0670 | Error | Live | ImplMemberMissing | An impl Trait for Type does not provide a member its trait declares (completeness). Every trait member is an obligation: a missing rule-plane member would leave the member predicate without a clause for this target, so calls covered by this impl would silently derive nothing. Provide the member with a full body, or remove the impl. |
| OE0671 | Error | Live | ImplMemberExtraneous | An impl Trait for Type provides a member the trait does not declare, or provides it with a different signature — arity, parameter types modulo Self, return type, or plane (derive/check/query vs fn/mutate). The trait owns the member contract; an impl can neither extend it nor reshape it. Bare impls (impl Type { … }) are exempt — they carry no contract (§5.4). |
| OE0672 | Error | Live | OrphanImplViolation | An impl Trait for Type lives in a package that declares neither Trait nor Type (§12.4’s reserved orphan rule, now real). Allowing orphan impls would let two packages give one (trait, type) pair conflicting clauses with no owner to arbitrate. Move the impl into the package declaring the trait or the one declaring the type. v0.1 compiles a single user package plus the stdlib, so this fires today only for impls pairing a stdlib trait with a stdlib type; it activates fully with multi-package composition. |
| OE0673 | Error | Live | ImplTargetsOverlap | Two impls of one trait have overlapping targets: the targets are <:-comparable, or they share a declared common descendant (coherence). Overlap would put some declared type under two clauses, so clause selection would no longer be single-valued per declared type — and specialization (“more-specific impl wins”) is rejected by design. impl T for Person + impl T for USPerson is the comparable arm (write disjoint targets, or one impl whose body branches); impl T for Person + impl T for Customer with pub type Employee <: Person, Customer declared is the common-descendant arm. |
| OE0674 | Error | Live | SupertraitUnsatisfied | An impl Sub for T exists but a supertrait of Sub has no impl covering T. Supertraits are requires-constraints, Rust-exact: pub trait Sub: Super makes impl Sub for T well-formed only when some impl Super for U with T <: U exists. Nothing is inherited — add the missing supertrait impl. |
| OE0675 | Error | Live | SelfMisuse | Self is used outside the positions where it resolves. Self resolves only inside trait and impl bodies: in a trait member signature it is the obligation’s type parameter; in an impl member it denotes the impl’s target type. The full discipline: (1) Self outside a trait/impl body never resolves — it is not a module-level type; (2) a trait member signature must mention Self in at least one parameter position (a member that is not about the implementing type belongs at module level); (3) return-position Self on a query member is refused — it would make the dispatch endpoint’s type a union over impl targets, which needs union types or bounded generics (V1). Move the declaration into a trait/impl body, or replace Self with the concrete declared type. |
| OE0676 | Error | Live | ImplMemberSeverityDiverges | An impl’s check member states a severity different from the one its trait pins. A trait-side check member signature may pin its severity (check OverLimit(Self) => Severity::Error;); the pin makes the member’s blocking behavior part of the trait contract — a contract whose blocking behavior varies by implementor is a weak contract (an Error that guards mutations on one target but merely warns on another is not one obligation, it is two). Under a pin an impl may omit its payload’s severity: field (it inherits the pinned one — the ergonomic point of pinning) or restate the same severity (harmless); stating a different one is refused here, naming the trait, the member, the pinned severity, and the divergent one. Codes and messages stay per-impl. To keep per-impl severity freedom, remove the pin from the trait signature. |
Modules, attributes, temporal, modal, forget (07xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0701 | Error | Reserved | InvertedDuration | Metric temporal operator declared with bounds [a, b] where a > b (§7.3.2). |
| OE0702 | Error | Reserved | MetricArityMismatch | Unary metric temporal operator applied to two operands (or binary applied to one) (§7.3.2). |
| OE0703 | Error | Reserved | MetricAtomBelowTier | Metric temporal atom appears under temporal: snapshot or temporal: none (§6.10.5). |
| OE0704 | Error | Live | ReservedIntrinsicName | A declaration is named after an IDENT-lexed reflection intrinsic (implements, implementors, spec §4.4). Rule-body call forms of these names always denote the intrinsic, so the declaration would be silently unreachable. Rename the declaration. The keyword intrinsics (iof, specializes, meta, extent) cannot collide and need no gate. |
| OE0705 | Error | Live | UnknownDirective | An #[...] attribute names no registered compiler directive (the directive registry). Unknown attributes never silently no-op: a typo’d directive (#[defeasable]) would otherwise silently change rule semantics — the canonical case let a felon vote. The diagnostic suggests the nearest registered directive when one is within typo distance. Macro-invocation attributes (spec section 13) are not built; until they are, every attribute must be a registered directive. |
| OE0706 | Error | Live | DirectiveReservedUnimplemented | A directive the book documents but the toolchain does not implement (#[brave], #[intrinsic], #[coinductive], …). Refused loudly rather than silently ignored: a directive that parses green while doing nothing misrepresents program semantics. The message cites where the directive is documented and what to do instead. (Removed surfaces — the #[strict]/#[defeasible]/#[defeater] strength triple and #[priority] — refuse under OE0722 instead, with a migration hint.) |
| OE0707 | Error | Live | DirectiveInvalidPosition | A registered, executed directive attached to a declaration kind it does not apply to (e.g. #[static] on a derive rule, #[comptime] on a mutate). Previously ignored silently; the registry names the directive and its valid positions. |
| OE0708 | Error | Reserved | ReservedAttributeName | A user pub macro or #[procmacro] pub fn declaration shadows a compiler-reserved attribute name (§9.2). |
| OE0709 | Error | Live | AttributeArgsNotYetImplemented | Attribute argument form not yet implemented; only the bare form is admitted (e.g. #[comptime], not #[comptime(strict)]). The argument syntax is parser-reserved for a future release. |
| OE0710 | Error | Reserved | ModuleExtractionConservativityViolation | Module extractor cannot prove conservativity under the importing module’s world assumption (§3.5). |
| OE0711 | Error | Reserved | ModuleExtractionDefeatChainBroken | Defeat-aware extraction cannot close the defeat graph — a referenced defeater is excluded by the seed signature (§3.5). |
| OE0712 | Error | Reserved | ModalTemporalCrossNestRefused | Modal operator nests a metric temporal operator (or vice versa) at tier: recursive. Route to tier: fol (§9.1). |
| OE0714 | Error | Reserved | DirectiveConflict | Two directives from one mutually-exclusive family on a single declaration. Its only family was the rule-strength triple #[strict] / #[defeasible] / #[defeater], removed — each member now refuses individually as removed (OE0722), so a conflict never forms and this code has no live emitter. Reserved for a future exclusive-directive family rather than reused (registry hygiene). |
| OE0715 | Error | Live | ModuleHeaderReadingRemoved | A content-carrying file declares mod NAME; (no sibling NAME.ar, no inline { … } body) in the legacy module-header position — the reading where the mod renamed the file’s OWN namespace. mod NAME; has a single meaning, Rust’s: it declares a CHILD module backed by a sibling NAME.ar (or NAME/mod.ar). A file cannot rename itself. Refused loudly: the silent-rename meant a typo’d mod quietly relocated a whole file’s declarations under a phantom namespace while ox check stayed green. To name a package’s module, use the directory/file layout (the root file is module root); to declare a child module, create the sibling NAME.ar; to keep declarations at the current namespace, delete the mod NAME; line. |
| OE0716 | Error | Live | DefeatTargetUnresolvable | A #[defeats(target(args))] target resolves to nothing: no derive head of that name, no clause carrying that #[label], or no trait member at that @ Type impl in the module. Defeat targets are resolution-checked at elaboration (goto-def-able) so a typo never silently changes which norm a program concludes. Also covers a malformed call-form target and a bare #[defeats] with no target. The accepted forms are head(args), head.label(args), and Trait::member(args) @ Type. |
| OE0717 | Error | Live | DefeatsStrictConclusion | A #[defeats(...)] target resolves to a head/clause not marked #[default]. Strict conclusions are unattackable: adding rules to a classical program can only add conclusions, and defeat exists only where overridability was declared. Mark the targeted clause #[default] to make it overridable, or remove the attack. |
| OE0718 | Error | Live | DefeatGraphCycle | The defeat graph (over resolved rule identities — clauses and heads) has a cycle, refused in v1. Cyclic attack structures are exactly where the well-behaved compilation stories diverge; v1 refuses rather than picking one silently. An exception-to-an-exception (a #[default] rule that is itself a #[defeats] target) is legal as long as the chain bottoms out. The message names the cycle rule by rule. |
| OE0719 | Error | Live | DuplicateClauseLabel | Two clauses of one head carry the same #[label(name)]. Labels are per-head identities — head.label must address exactly one clause — so a head cannot have two clauses labeled alike. Rename one of the labels. |
| OE0720 | Error | Reserved | NonUniformBridgeRefused | Bridge consults facts at a different valid-time than the bridge head’s; refused outside tier: fol (§10.3). |
| OE0721 | Error | Live | DefeatArgumentUnbound | A #[defeats] target argument binds in neither the head nor the body of the decorated rule, or binds only in the body (per-tuple defeat keys on the attacker’s HEAD tuple). A directive argument is never a fresh variable — the lesson that a silently-fresh variable matched everything. Name a variable the attacker derives in its head. |
| OE0722 | Error | Live | DeprecatedStrengthAttribute | The Governatori strength-attribute surface — #[strict] / #[defeasible] / #[defeater] and #[priority] / pub priority — is removed. It silently inverted a head’s polarity at a distance (a defeater spelled the head it denied) and numeric priority was non-compositional and silently dropped. The replacement is honest heads plus the defeat-directive plane: unmarked rules stay strict; #[default] marks an overridable clause; an exception is an ordinary rule under its own honest head carrying #[defeats(target(args))]; lex specialis is the specific rule defeating the general clause’s #[label]. No silent aliasing — migrate the program. The help text shows the new form for the specific attribute. |
| OE0723 | Error | Live | MacroInvocationDidNotExpand | A macro invocation name!( … ) survived the EXPAND phase: no pub macro name with a rule matching the invocation is in scope (the macro is undefined in this module, or none of its rules’ matchers bind these arguments). An unexpanded invocation is refused rather than silently accepted — an invocation the elaborator does not understand must not vanish from the artifact (the silent-wrong discipline forbids). Define the macro, fix the invocation to match a rule, or import the macro (cross-module macro use is a follow-up). |
| OE0724 | Error | Live | UnknownFragmentSpecifier | A macro metavariable $name:spec names a fragment specifier outside the closed v1 set. The admitted specifiers are the ontological concept / rel / metatype / rule and the syntactic expr / ident / ty / literal / path / tt; standpoint is cut from v1. Validated at expansion so a macro body never binds a metavariable against a parse category the expander cannot honor. Use one of the admitted specifiers. |
| OE0725 | Error | Live | ReservedHygieneIdentifier | A user-authored identifier contains the reserved hygiene marker · (U+00B7 MIDDLE DOT). That marker is reserved for the names macro hygiene mints when freshening a macro-introduced variable, giving those names a namespace provably disjoint from anything a user can write — the disjointness the hygiene non-capture guarantee rests on. Remove the · from the identifier. |
| OE0726 | Error | Live | DuplicateMacroDefinition | A module declares two macros with the same name. A macro name must have exactly one implementation per module (uniqueness-at-registration) — two macro name { … } declarations are a loud error, never a silent merge or shadow (the silent-wrong discipline forbids; the two rule sets would otherwise be invoked as if one macro). Rename or remove one of the declarations; a distinct cross-module macro of the same name is reached with a module-qualified use. |
| OE0727 | Error | Live | MacroExpansionDidNotConverge | A module’s macro expansion did not reach a fixed point within the fuel budget: a macro whose expansion re-introduces an invocation of itself (directly or through a cycle) diverges. The phase stops and refuses rather than looping forever — distinct from OE0723 (an invocation that no rule matches), since here the macro does match but its expansion never settles. Break the recursion (add a non-recursive base rule, or stop the rule from re-emitting its own invocation). |
| OE0728 | Error | Live | ExpansionSpanUnanchored | A diagnostic produced over macro-expanded text carried a span that the source-faithful expansion provenance map could not resolve to any location the user wrote. This is a compiler bug: every span over expanded input must resolve to real user source (a copied-through fragment) or to the invocation site of the macro that synthesized it. The compiler refuses to render text the user never wrote, so it surfaces this internal error loudly rather than pointing at a phantom location. Please report it with the input that triggered it. |
| OE0729 | Error | Live | ProcmacroBodyNotTotal | A procedural macro (#[procmacro] pub fn) body uses a construct outside the total fragment, so its termination cannot be guaranteed by construction. The procedural meta-language is deliberately a closed, strongly-normalizing fragment: let bindings over string literals / reflection projections (item.name, item.params[i].ty) / concat_idents(…) paste, a quote { … } (with $-splices and a bounded $( for x in item.fields ) { … } repetition over a reflected child-list), and a list of quote/typed-artifact elements. General control flow (if / match / for / while statements) and calls to anything other than concat_idents are refused HERE, at the declaration — totality is enforced, not trusted to a fuel cap at expansion time (distinct from OE0727, which catches a runaway at expansion). Rewrite the body within the total fragment; iteration over a declaration’s structural children is the bounded $( for … ) repetition inside quote, not an open loop. |
| OE0730 | Error | Live | ForgetWithoutCapability | A mutate body contains forget but the enclosing mutate declaration does not grant #[allow_forget]. forget physically erases axiom events including their bitemporal history; the capability must be granted explicitly at the declaration that wields it. The richer capability plane (ox.toml / backend principal / _forget_log audit) is a recorded follow-on. |
| OE0731 | Error | Reserved | ForgetLogTamper | Attempted forget on a _forget_log event. |
| OE0732 | Error | Reserved | RetentionViolation | Query targets transaction-time outside the declared #[retention(tx_time, ...)] window. |
| OE0733 | Error | Live | SchemaDeclInStandpoint | A schema-level or rule/invocation declaration appears inside a standpoint { … } block. The refused forms are the vocabulary declarations (pub type / pub rel / metaxis / metatype / metarel / enum / struct) AND the rule/invocation forms (derive / check / query / mutate / bridge / fn / trait / sink / macro); only pub fact / pub not_fact (and mod / use / test / a nested standpoint) are admitted. A standpoint contributes a SOURCE’S VIEW of ground truth — facts — not the universe’s vocabulary or its rule/invocation/derivation logic; those describe the universe and belong at module level. The elaborator silently DROPPED these (the decl vanished from the artifact: pub query against a standpoint-nested type then failed unresolved type, proving it never existed), while the checker half-saw them (it reference-checked a standpoint-nested rel against a sibling decl it could not see, mis-reporting unresolved type for a type sitting two lines above) — a silent drop plus a check/lowering split-brain. Refused at ox check / ox build, parallel to the impl-in-standpoint refusal added to the same dispatch. Move the schema declaration to module level; only facts are standpoint-scoped. (Per-standpoint schema scoping is a recorded open design question, not a v0 feature.) |
| OE0734 | Error | Live | ProcmacroSignatureType | A procedural macro (#[procmacro] pub fn) signature names a type outside the reflection vocabulary. A procmacro is a macro definition, not a value-level function: its parameters reflect the macro’s input — Decl (the decorated declaration), Args (the attribute arguments), or Syntax (a function-like macro’s token input) — and it returns Syntax (a single emission) or Expansion (a list of emissions / typed artifacts). These reflection types are the only ones admitted in a procmacro signature; an ordinary value type (or a typo) is refused here so the macro author is not silently handed a meaningless signature. Use one of the reflection types. |
| OW0710 | Warning | Reserved | OrphanModuleFile | Reserved (no emitter): an .ar file under the package source root reached by no mod/use chain from the entry is not part of the package (Rust module semantics, §3.1) — not compiled, not checked, not linted. Argon matches Cargo and says NOTHING about such a file: an unreferenced source file is simply not compiled, not surfaced as a warning. The orphan set is still computed (the workspace records it; a future tool may surface it as an editor hint), so the code is held rather than retired. Declare the file with a mod item from a reachable module to include it. |
Field modifiers, world-assumption gates, construction (08xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0810 | Error | Reserved | WorldAssumptionTemporalIndex | #[world(...)] declared per concept with a valid-time scope but no standpoint context (§6.9). |
| OE0811 | Error | Reserved | PersistenceMarkerOnNonTemporalRule | Persistence marker applied to a rule outside the temporal substrate. |
| OE0812 | Error | Reserved | DefeasibleDefeaterOfDefeasibleTemporal | Defeasible defeater of a defeasible temporal rule; route to unsafe logic (§7.8). |
| OE0820 | Error | Live | UpdateImmutableField | An update target: T set { f = … } writes to a field f that T declares without the mut modifier. The elaborator validates each field-assignment against the field-decl’s mut flag and refuses at build time, not at mutation invocation. Emitted by the mutate-body lowerer (oxc-instantiate); a bare (un-annotated) target defers to the runtime rather than emitting here. |
| OE0822 | Error | Reserved | MutOnDerivedField | Reserved (designed — §5.1): a mut modifier on a from-navigation-derived field, which is contradictory — a derived field has no independent value to mutate. mut is otherwise orthogonal to the other field-decl modifiers (#[intrinsic], defaults, metatype rigidity). This entry reserves the code for that gate. |
| OE0831 | Error | Live | PositionalInsertArityMismatch | A positional insert C(a, b, …) targets a concept C. The mutate-body lowerer only routes the brace form insert C { field: … } to construction, so a positional concept insert silently constructs nothing; it is refused at check. The diagnostic names the required fields in constructor order and (on an arity mismatch) reports the argument-vs-field counts. Emitted by the driver’s mutate-body validation (oxc-driver). Use the brace form insert C { field: … } to supply fields by name. |
World-assumption conservativity (09xx)
Reserved range — the §6.9 world-assumption gates are designed but not yet wired.
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE0901 | Error | Live | MixedWorldAssumptionConflict | A rule or query applies negation-as-failure (not R(..)) to a FOREIGN relation (one mapped to a store by [placement.R]) whose source is read open-world — the CWA-importing-OWA boundary. A foreign relation is materialized into the fixpoint as a frozen CLOSED-world slice (the frozen-EDB rule), so not R(..) reads absence-in-the-slice as definite-false; but absence of a tuple in an OPEN-world source is UNKNOWN, not false (§6.9), so the negation reads unknown as definitely-false and over-asserts its head. The closed-world lifting of the slice into the open consumer is not provable — the TypeSystem/Soundness/CwaOwa.lean conservativity theorem proves the CWA→OWA transfer sound in one direction only — so the negation is refused at ox check / ox build rather than silently evaluated, the C6×D4 hinge (a CWA relation admits NAF; an OWA one does not). To resolve: mark the relation’s source closed-world (so absence IS falsity and not R is sound), or restructure so the foreign relation is not negated. The native-concept analog — NAF over a #[world(open)] concept’s own derived extent — is the separate OE1367 gate (the evaluator threads it three-valued); this code is the federation boundary, where the frozen slice is a closed snapshot of an open source. |
| OE0903 | Error | Reserved | WorldAttributeOnStruct | Reserved — superseded by OE0707: #[world(...)] applied to a struct/enum, which carry no ontological classification — world assumption applies only to classified concepts. The world directive is now registered concept-only (positions: [CONCEPT_DECL]), so a struct/enum placement refuses via the generic placement gate OE0707; this bespoke code is retained as a reserved row and does not fire. |
| OW0902 | Warning | Reserved | WorldAssumptionImplicit | Reserved (designed — §6.9): default_world omitted from ox.toml, so the substrate applied a default world assumption rather than an explicit modeler choice. The warning that would suggest making the choice explicit is unbuilt; this entry reserves the code. |
CWA evaluation and field intent (10xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1014 | Error | Live | RequiredFieldUnasserted | Under the closed-world default (book §6.9 — CWA everywhere today), a required field’s value is not derivable from any asserted axiom: the schema declares the field present but the KB does not assert it. Emitted by the runtime mutate path (oxc-runtime) at exactly one site: the in-body insert iof(x, T) construction-completeness gate — classifying x into T whose required fields x lacks, when the same body also populates fields of x, refuses atomically, judged at body end so insert iof; update set { … } in one mutation is complete (read-your-writes). Distinct from brace construction (insert T { … } missing a field → OE0207). Staged construction (classify in one mutation, populate later — no in-body field writes) currently emits NO diagnostic: the absent required field reads as K3-unknown and collapses to CWA-false at access. The field-access-time / evaluation-channel emitter that would surface staged incompleteness is designed and is not built in v0. #[world(open)] is a live concept attribute; the permit-as-unknown softening of this required-field gate under an open-world concept is designed and not yet wired. |
| OE1016 | Error | Reserved | Truth4OfOnStruct | Truth4Of<T> field type appears on a struct (no metatype classification). The four-valued surface is restricted to metatype-classified concepts (pub type ..., or a declared vocabulary introducer). |
| OW1015 | Warning | Reserved | OptionalFieldAmbiguousIntent | Reserved (no emitter): an optional field field: T? does not record whether its optionality is structural (the value may legitimately be absent) or epistemic (the value exists but is unknown), a distinction drawn from JPA’s TBox/SHACL split. The intent-marking surface and the warning that would request it are unbuilt — the #[intent(...)] attribute does not exist yet (the lint-level #[allow(...)] / [lints] suppression surface does, and this code is wired to the optional-field-ambiguous-intent lint, so the warning will be suppressible the moment it gains an emitter). This entry reserves the code for that future surface; today every T? field checks clean. |
Bridges (11xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1101 | Error | Reserved | BridgeCycle | The bridge graph (nodes = standpoints, edges = bridges) contains a cycle that prevents stratified evaluation. Cycles between standpoints under bridge composition would require fixed-point recomputation across the federation; v0.1 admits only acyclic bridge graphs. Either break the cycle (drop one of the bridges) or wait for the V1 #[managed] bridge surface (spec §10.3). |
| OE1102 | Error | Live | BridgeNotEvaluated | A pub bridge declaration (§10.3). Bridge rules parse, lower to BridgeDecl wire events, and resolve, but the federation fixpoint never fires them (Module::bridges_targeting has no callers) — a built artifact’s bridge bodies would silently never contribute to their target standpoint. Refused at ox check / ox build rather than building an artifact whose declared inference is inert. Bridge evaluation is the post-stable flagship standpoint deliverable; the parse / lower / wire / decode surface is retained so the work resumes without a grammar or wire-format change. Model the cross-standpoint inference with an explicit derive / pub fact for now. |
| OE1103 | Error | Live | UndeclaredStandpointInAcross | A pub query … across [ … ] clause names a standpoint that is not declared in the workspace. Under the standpoint visibility composition rule (§10.4), a federated query reads the named standpoints’ views info-joined with the DEFAULT layer; a name that resolves to no standpoint declaration would silently contribute the DEFAULT layer alone (an empty per-standpoint extent), masking the typo as a real — but wrong — federation result. The query is refused loudly at dispatch naming the unknown standpoint. Declare the standpoint, or correct the name in the across clause. Mechanization: Argon.Visibility (the visibility relation the federated materializer honors). |
Build composition and .oxbin validation (12xx, §16)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1201 | Error | Live | InvalidMagic | .oxbin magic header does not match \0oxbin\0\1. |
| OE1202 | Error | Live | IncompatibleVersionAxis | Major bump on one of the four version axes (oxbin_format_version, core_ir_version, tier_ladder_version, runtime_contract_version). |
| OE1203 | Error | Live | UnknownMandatorySection | .oxbin carries a MANDATORY-flagged section type the runtime does not recognize. |
| OE1204 | Error | Live | TierMismatch | Artifact’s max_tier_claimed or max_temporal_claimed exceeds the runtime’s supported maxima. |
| OE1205 | Error | Live | ArrangementSectionUnsupported | MVP runtime refuses an .oxbin carrying a DBSP-shape arrangement-section. |
| OE1210 | Error | Live | SymbolResolutionFailed | Layer-2 validation: a reference in events / rules / queries / mutations did not resolve to a symbol-table entry. |
| OE1211 | Error | Live | StandpointLatticeCycle | Standpoint lattice has a cycle (compose-time or load-time). |
| OE1212 | Error | Live | ProvenanceNotDNF | An event’s derivation column is not a well-formed PosBool(M) DNF. |
| OE1213 | Error | Live | CompositionSignatureMismatch | Stored composition_signature does not match what its four legs compute. |
| OE1214 | Error | Live | TierTableInconsistent | A rule’s tier in tier-table exceeds the artifact’s max_tier_claimed. |
| OE1215 | Error | Live | DocLinkUnresolved | A DocBlock cross-link does not resolve; strict policy fails, lenient warns. |
| OE1220 | Error | Live | UnknownAxiomKind | Events section carries a variant tag the runtime does not recognize. |
| OE1230 | Error | Live | TierCapExceeded | Compose-time: a declaration’s classified decidability tier exceeds the workspace ceiling set by [lattice].max_tier in ox.toml (§10). The ceiling is the highest of the seven ladder tiers (structural, closure, expressive, recursive, fol, modal, metaorder) a package admits; a rule the §10 classifier places above it is refused at ox check/ox build rather than emitted into an artifact that claims a tier it exceeds. Raise max_tier, or rephrase the declaration into the admitted fragment. |
| OE1240 | Error | Live | DependencyVersionUnsupported | An ox.toml [dependencies] entry uses a registry/VCS form — a bare version string (dep = "1.0") or one of the version/git/branch/tag/rev/registry keys — that is not yet supported. v1 supports path dependencies only (dep = { path = "../dep" }). The form is recognized and refused feature-named, not silently ignored: a version requirement that silently resolved to nothing (or to a path) would be silent-wrong. Use a path dependency, or wait for the registry story. |
| OE1241 | Error | Live | DependencyNameMismatch | An ox.toml [dependencies] key does not match the [package].name of the package at the declared path. The dependency name is the path root the consumer uses (use dep::X;, dep::X), and v1 keeps the dependency’s published name authoritative (Cargo’s default). Rename the [dependencies] key to the package’s published name, or fix the path. |
| OE1242 | Error | Live | DependencyCycle | The [dependencies] graph contains a cycle. Package dependencies must form a DAG; a cycle is refused loudly naming the cycle path (a -> b -> a). Break the cycle — extract the shared surface into a third package both depend on. |
| OE1243 | Error | Live | DuplicateDependencyName | A dependency name collides as a path-resolution root: either the same name resolves to two different package directories across the dependency graph, or it equals an intra-package top-level module name. A name is a single namespace root; it cannot denote two things, and a bare name::X must not silently prefer one. (Two declarations resolving to the SAME canonical directory are de-duplicated, not an error.) Rename the conflicting dependency or the local module. |
| OE1244 | Error | Live | DependencyPackageLoadFailed | A path dependency could not be loaded: the path does not point at a directory with an ox.toml, the dependency’s entry/module closure failed to resolve, or its manifest did not parse. The dependency is refused naming the path and the underlying cause rather than silently contributing nothing. |
| OE1245 | Error | Live | MappingSchemaDrift | A compiled placement/mapping artifact pins to a Schema composition signature that does not match the schema it is being loaded against. The mapping compiles to a content-addressed artifact hashed against the schema’s composition signature, so a schema change that the mapping was not recompiled against is a LOAD-TIME refusal rather than a runtime surprise: a relation could otherwise be mapped to a store under a shape the schema no longer has. Placement is versioned separately from the schema — a relation can move stores without a schema bump — but the mapping always pins to the schema hash it was compiled against. Recompile the mapping against the current schema (ox build). |
| OE1246 | Error | Live | ForeignRelationAlsoDerived | A relation is BOTH mapped to a foreign store by a [placement.<rel>] section AND produced by a derive rule. A relation’s extent must have a single origin — it is EITHER extensional (its rows live in the foreign store the placement points at) OR intensional (its rows are computed by rule derivation) — never both. Allowing both would silently produce an ambiguous extent: the foreign rows and the derived rows would compete, and which one a query reads would depend on evaluation order rather than on the model. The conflict is refused at build time naming the relation, rather than emitting an .oxbin whose foreign-vs-derived extent is undefined. Either drop the derive rule (the relation is foreign-owned) or remove the [placement] (the relation is rule-computed). |
| OE1247 | Error | Reserved | ConflictingPlacement | Reserved (designed, not yet wired): a relation has a placement declared from two sources at once — e.g. an ox.toml [placement.<rel>] section AND a source-level foreign!/#[foreign] macro, or the same relation placed twice. A relation may be bound to at most one store; two placements would compete for which store backs the relation’s extent, and which one wins would depend on merge order rather than the model. The placements are unioned at build time (MappingSections::merge_placements) and a relation appearing in more than one source is refused, naming the relation, rather than silently overwriting one with the other. The union seam exists; the build path that emits this code on a macro-emitted-vs-config collision is wired when the macro emission surface lands. Keep a single placement for the relation — either the ox.toml section or the macro, not both. |
| OE1248 | Error | Live | WorkspaceInheritanceUnavailable | A [package] field uses the workspace-inheritance form { workspace = … } but there is no enclosing [workspace] to inherit from, or the value is workspace = false. Per-field inheritance (version.workspace = true, edition.workspace = true) is meaningful only for a member of a workspace whose root declares the shared [workspace.package] field. Set workspace = true inside a real workspace, or write the value literally. |
| OE1249 | Error | Reserved | UnknownEdition | Reserved (no emitter) — edition-value validation deferred until editions gate surface syntax. Editions are non-negative integers, opt-in per package, parse-time-only and inert in v1: the resolved value gates nothing downstream, so any integer (given as edition = 1 or the legacy string edition = "1") is accepted. This code is held so that once an edition gates real surface syntax, an edition this toolchain does not understand can be refused feature-named rather than silently treated as the default. |
| OE1250 | Error | Live | WorkspaceMemberVersionUnsatisfied | A workspace member depends on a sibling member (or a path dependency) whose declared [package].version does not satisfy the depending package’s SemVer constraint. A member-to-member dependency resolves LOCALLY to the sibling member directory — never to a registry — so the sibling’s declared version must satisfy the constraint, or the workspace is internally inconsistent. Align the member version, or relax the constraint. |
| OE1251 | Error | Live | PatchTargetUnsupported | A root-only [patch.<source>] entry targets a registry/VCS source that is not yet supported. v1 patches only path/workspace sources; a registry patch is refused feature-named rather than silently ignored, since the registry does not yet exist. Patch a path/workspace source, or wait for the registry story. |
| OE1252 | Error | Live | WorkspaceMemberGlobEmpty | A [workspace] members glob matched no package directory. A member glob must resolve to at least one directory holding an ox.toml; an empty glob is refused rather than silently producing a member-less workspace (a typo in members = ["crates/*"] would otherwise drop every member without a word). Fix the glob, or remove it. |
| OE1253 | Error | Live | WorkspaceMemberGlobMalformed | A [workspace] members/exclude/default-members glob is malformed: an invalid glob pattern, or an unreadable matched path. The pattern is refused naming the glob and the underlying cause rather than silently expanding to nothing. |
| OE1254 | Error | Live | WorkspaceInheritedFieldMissing | A workspace member inherits a field with <field>.workspace = true (or <dep>.workspace = true) but the workspace root’s [workspace.package] / [workspace.dependencies] does not declare that field. Inheritance pulls the shared declaration from the workspace root; if the root does not declare it, there is nothing to inherit. Declare the field under [workspace.package] / [workspace.dependencies], or give the member a literal value. |
| OE1255 | Error | Live | WorkspaceDependencyOverride | A workspace member inheriting a dependency with <dep>.workspace = true overrides a field of the inherited [workspace.dependencies] declaration beyond what is permitted. A member may add only optional / features over the inherited declaration (additive); it may not redeclare the source (path/version/git/…). Drop the override, or stop inheriting and declare the dependency in full. |
| OE1256 | Error | Live | WorkspaceDefaultMembersEmpty | A [workspace] default-members list resolved to no package directory. A declared default-members must name at least one existing member; a list that matches nothing is refused rather than silently building an empty default set (a typo would otherwise drop every member without a word). Fix the default-members entries, or remove the key to default to all members. |
| OE1260 | Error | Live | LockfileMalformed | An ox.lock lockfile failed to parse. The lockfile is the machine-generated record of the resolved package graph; a hand-edited or truncated lock is refused naming the parse cause rather than silently regenerated, since silently overwriting a corrupt lock would hide a merge conflict or a botched edit. Delete the lock to regenerate it (ox build), or fix the malformed entry. The lockfile is auto-generated — do not edit it by hand. |
| OE1261 | Error | Live | LockfileContentHashMismatch | A package’s recomputed content_hash does not match the value recorded in ox.lock. The lock pins each resolved package to a deterministic BLAKE3 hash over its source (the .ar files plus a normalized ox.toml); a mismatch means the on-disk source changed under a committed lock. Outside --locked/--frozen the lock is refreshed automatically on the next resolve; under --locked/--frozen it is a loud refusal, because a CI build must not silently build different source than the lock pins. Re-run without --locked to refresh the lock, or restore the pinned source. |
| OE1262 | Error | Live | LockfileDrift | The resolved package graph does not match the committed ox.lock under --locked/--frozen: a package was added, removed, or its version/source/content_hash changed since the lock was generated. --locked requires the lock to already describe the resolution exactly and refuses to regenerate it, so drift is a loud refusal rather than a silent rewrite — the strict-CI contract that the build matches the committed lock. Re-run without --locked to update the lock, then commit the updated ox.lock. |
| OE1263 | Error | Live | LockfileMissing | No ox.lock exists at the workspace root, but --locked/--frozen was requested. These flags require a committed lockfile to verify the resolution against; in their absence there is nothing to honor, and silently generating one would defeat the strict-CI guarantee that the build matches a reviewed lock. Run ox build (or ox check) without --locked once to generate and commit ox.lock, then re-run with the flag. |
| OE1264 | Error | Live | UnresolvableDependencyGraph | The dependency graph has no solution: there is no single assignment of one version per package that satisfies every requirement. Argon resolves each package name to EXACTLY ONE version graph-wide — unlike Cargo, it never lets two semver-incompatible versions of the same package coexist under name-mangling, because two concepts at one qualified path cannot both be ‘the’ type (the nominal-identity invariant). So two requirements that pin the same package name to disjoint version ranges are an unsatisfiable conflict, refused loudly rather than silently mangled into coexistence. The diagnostic carries the PubGrub derivation chain — the root-cause sequence of ‘because A depends on B and C requires not-B’ steps that proves no solution exists. Relax a version constraint, align the conflicting requirements on a shared version, or drop one of the conflicting dependencies. |
| OE1265 | Error | Live | RegistryFetchFailed | A registry read failed: a transport/network error, an unreadable file:// index path, a malformed index record, or an offline build with no warm cache entry. The registry is a static content-addressed store (a config.json plus an append-only NDJSON sparse index over https:///file://); a read that cannot answer is refused loudly rather than treated as an empty result, because a silently-empty index would resolve a real dependency to nothing (silent-wrong). Check the registry URL/path is reachable, or build --offline against a warm cache. |
| OE1266 | Error | Live | RegistryContentHashMismatch | A fetched package artifact’s BLAKE3 hash does not match the cksum the registry index recorded. The content hash is the artifact’s content-address: a mismatch means the artifact is corrupt, truncated, or tampered. This is a FAIL-CLOSED refusal — the artifact is never cached or built, so a bad registry object cannot enter the build. Re-fetch; if the mismatch persists the published object is bad. (Cryptographic signatures over the artifact are out of scope; v1 integrity is the content hash only.) |
| OE1267 | Error | Live | NoRegistryConfigured | A registry dependency is declared but no registry is configured. A bare version requirement (dep = "1.0") or a { version = "1.0" } form that names no local workspace member is a REGISTRY dependency — it resolves against a configured sparse index. This REPLACES the old OE1240 ‘registry form reserved for later’ refusal: the form is supported now, it just needs an index. Add a [registry] table to ox.toml (index = "file:///path" or index = "https://…"), or set ARGON_REGISTRY; to depend on a local checkout instead, use a path dependency (dep = { path = "../dep" }). |
| OE1268 | Error | Live | RegistryPackageNotFound | The package — or a version satisfying the requirement — is not in the registry index. The sparse index has no record file for the package name, or the version requirement matches no published version. Refused loudly rather than resolved to nothing. Check the package name and the version requirement, and that the package has been published to this registry. |
| OE1269 | Error | Live | RegistryYankedVersionSelected | The only version satisfying the requirement is yanked, and the requirement is not an exact pin. A yanked version is withdrawn from new resolutions — it stays selectable only when pinned exactly (= x.y.z), e.g. from an existing ox.lock that already pinned it, so a yank never breaks a locked build but is not picked for a fresh resolve. Pick a non-yanked version, or pin exactly if you must depend on the yanked one. |
| OE1270 | Error | Live | RegistryArtifactTooLarge | A registry body (an index record or a package artifact) exceeds the transport’s hard size cap. A registry read buffers the whole body before its content hash can be checked (OE1266 only protects AFTER the body is in memory), so an unbounded body is a denial-of-service: a malicious or buggy registry can exhaust memory or hang the build on a slow drip. The cap (a build-time constant, 256 MiB — far above any plausible published SOURCE package) is a hard refusal, not a truncation, and is enforced both before the download (when the index declares a size over the cap) and during it (the read itself is bounded); the server’s Content-Length is never trusted. If a package is legitimately this large, the cap must be raised deliberately. |
| OE1271 | Error | Live | RegistryInvalidPackageName | A registry dependency name is not a valid package identifier. A package name is used VERBATIM as a filesystem path segment — the index shard path and the content-addressed cache path under ~/.argon — so it must match [A-Za-z0-9_-]+ (non-empty, ASCII alphanumerics plus _ and -): no path separators, no ./.. components, no empty string. A name like ../../tmp/x would otherwise escape the cache root and write outside it; this is validated at the manifest boundary, before the name reaches any path join, and refused loudly rather than silently traversing. Fix the dependency name in ox.toml. |
| OE1272 | Error | Live | PublishManifestInvalid | ox publish refused because the package manifest is not publishable. A published package’s ox.toml must declare a [package] table with a non-empty name matching the registry charset [A-Za-z0-9_-]+ and a parseable [package].version — the index keys a version record by exactly that (name, version). A missing/empty name, a name with a path separator or .. (which would escape the index shard / cache path), or an absent version is refused loudly rather than published under a guessed coordinate. Fix [package] in ox.toml. |
| OE1273 | Error | Live | PublishVersionExists | ox publish refused because <name>@<version> is already published to this registry. The sparse index is APPEND-ONLY and every published artifact is immutable (content-addressed): a version is published exactly once and never overwritten, so a re-publish of the same coordinate — which would silently change what a downstream lock already pinned — is a hard refusal, not an in-place mutation. Bump [package].version and publish the new version; to withdraw a bad version use a yank (an append-only event), never an overwrite. |
| OE1274 | Error | Live | PublishRegistryWriteFailed | ox publish could not write to the target registry. v1 publishes to a LOCAL / file:// registry only — it writes the content-addressed blob under blobs/, appends the version record to the package’s NDJSON index shard, and ensures config.json — so the target must be a writable local directory or file:// URL. Publishing to an https:// production CDN registry needs upload + auth and is out of scope for v1. Check the registry path is a writable local directory, or point --registry / [registry].index at a file:// path. |
| OE1275 | Error | Live | PublishEmptyPackage | ox publish refused because the package has no publishable source. A package directory must hold an ox.toml and at least one .ar source file — an empty package (no .ar files), or a path that is not an Argon package directory at all, is refused loudly rather than publishing an empty artifact that would resolve to nothing downstream. Add source, or publish the correct package directory. |
| OE1276 | Error | Live | PublishBreakingChange | ox publish refused because the new version makes a BREAKING change to its pub declaration surface without the breaking-tier SemVer bump. Argon recomputes the constructs semantic signature of both the version being published and the latest previously-published version of the same package, diffs them per declaration, and classifies each change against the compatibility taxonomy — proven sound in the scratch-Lean oracle (binary-compatibility, JLS Ch.13 analog): a COMPATIBLE change (a new pub decl, a widened bound/refinement, grown <: parents, widened cardinality) may publish as a patch, but a BREAKING change (removing/renaming a pub decl, narrowing a refinement — including adding a not P (NAF) clause —, an arity / position-type change, narrowing cardinality, or a not-clause-bearing closed→open (CWA→OWA) world flip) requires the breaking tier: a MAJOR bump in 1.x+, a MINOR bump in 0.x (Cargo’s 0.MINOR.PATCH rule — a 0.MINOR bump is the breaking tier pre-1.0). This is refused loudly, naming the offending declaration(s), the kind of break, and the required bump, rather than silently publishing a version a downstream ^-range would auto-adopt and then fail to resolve/check against. Bump [package].version to the required tier, or make the change compatible. The first-ever publish of a package (no prior version) is unconstrained. |
| OW1240 | Warning | Live | UnusedManifestKey | An ox.toml section or key is not recognized by the compiler (Cargo-style “unused manifest key”). Recognized surface is [package]/[project]/[schema]/[dependencies]/[lattice]/[standpoints]/[store]/[placement]/[workspace]/[patch]/[registry]/[fmt]/[lints] and their known keys; anything else (a typo, a not-yet-supported section, an unknown [lattice].max_tier ladder name) is surfaced as a warning naming the section/key rather than silently dropped by deserialization. |
| OW1241 | Warning | Live | UnknownLint | A lint name in a #[allow/warn/deny(...)] attribute or the ox.toml [lints] table is not a registered lint. A lint name keys a level-controllable diagnostic (e.g. non-canonical-clause-order for OW0010); an unknown name is surfaced — with a did-you-mean suggestion when one is close — rather than silently ignored, because a typo’d name would leave the diagnostic at its default level with no signal. The level value itself (allow/warn/deny) is also checked: a [lints] entry whose value is not one of those words is reported here too. The configured lint set is OxcDiagnosticCode::ALL_LINTS. |
Runtime contract and rule-evaluation gates (13xx, §17)
ox build is fail-closed: it re-runs the runtime’s own rule compiler as an oracle over every rule and refuses to write an .oxbin if any rule will not evaluate. The aggregate/quantifier codes (OE1311–OE1315) are the gate’s refusals.
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1301 | Error | Reserved | UnsafeLogicTimeout | Query touching an unsafe logic { } rule exceeded the runtime’s wall-clock budget (§17.5). |
| OE1302 | Error | Reserved | FederationDisagreement | A federated query under #[federate(strict)] produced a row whose AFT info-join across contributing standpoints is Truth4::Both — at least one source asserts Is, at least one asserts Not. The K3 fail-closed projection at the query boundary rejects the disagreement. Switch to the default paraconsistent policy to receive the row tagged as Both, or reconcile the contributing standpoints’ axioms (§11.2). |
| OE1303 | Error | Live | RuleNotRangeRestricted | A reasoner rule is not range-restricted (unsafe): a variable in the head, inside a negated (NAF) atom, or as a comparison/compute operand is never bound by a positive body atom. Such a rule degrades silently — an unbound head variable projects Null, an unbound negated/compared variable mis-evaluates — so it is refused at compile. Bind the variable with a positive body atom (standard Datalog range-restriction / safety). The named offenders are genuine VARIABLES: every multi-segment path in a value position is resolved (enum constant / axis value / type reference / declared individual) or refused (OE0227 / OE0620) at elaboration, so a qualified path like rigidity::anti_rigid can no longer reach this error — an offender containing :: indicates an artifact built by a pre-S0 toolchain; rebuild from source. |
| OE1304 | Error | Live | NonComparisonOperator | A reasoner Comparison atom uses a non-comparison operator. Only == / != / < / <= / > / >= are meaningful as a rule-body filter; any other operator (e.g. +) makes the executor’s ordering test undefined and would silently drop every row, so it is refused at compile. |
| OE1305 | Error | Live | TierNotYetImplemented | Rule classified at a tier the current runtime executor does not yet support. |
| OE1306 | Error | Live | RuleExceedsExecutorTier | Rule’s classified tier exceeds the dispatched executor’s supported_tiers upper bound. |
| OE1307 | Error | Live | ComptimeNotStatic | Rule declared #[comptime] depends on runtime state (mutation, dynamic individual, or non-static relation). |
| OE1308 | Error | Live | ComptimeForbiddenOnMutate | #[comptime] is not admissible on mutate declarations. |
| OE1309 | Error | Live | StratificationNafCycle | NAF dependency cycle prevents stratified evaluation; use SLG (expressive tier) or rewrite. |
| OE1310 | Error | Live | SemiringMismatch | Provenance witnesses combined under incompatible semirings. |
| OE1311 | Error | Live | NestedAggregate | Nested aggregates (count { count { ... } > N }) require answer-subsumption (SLG-class) evaluation and are refused in v0.1. Flatten via an intermediate pub derive rule, or wait for the V1 SLG kernel. |
| OE1312 | Error | Live | AggregateKindNotImplemented | Collection/string aggregate kind not yet implemented. The folding kinds (count / count_distinct / sum / min / max / avg) evaluate at runtime today; collect / set_collect / string_join / percentile require the V1 SLG kernel (Expressive tier). The collection-kind subquery forms — collect { … } (List), set { … } (Set), one { … } (Option) — are refused under this code too: pre-refusal they silently lowered to count (a green-checked cardinality test instead of the documented collection semantics). Use count { … } / exists { … }, or an intermediate pub derive. |
| OE1313 | Error | Live | NafAggregateForbidden | Negation-as-failure (not) cannot wrap an aggregate subquery (count { … }, exists { … }) in v0.1 — the result is unsound (the inner aggregate was silently dropped by lowering, pre-γ.7). Rewrite via an intermediate pub derive rule that exposes the aggregate result, then negate over it: pub derive any() :- exists { p(x) }; pub derive none() :- not any();. Lift planned alongside the V1 SLG kernel. |
| OE1314 | Error | Live | EmptyAggregateBody | Empty aggregate body (count { }, exists { }) is refused — the resulting cardinality is the trivially-vacuous count of the surrounding binding (always 1), which is almost certainly not what the modeler intended. Provide at least one atom in the body, or use a constant predicate. |
| OE1315 | Error | Live | QuantifierNotYet | A quantifier rule atom is refused because the executor can’t yet evaluate this shape as a true quantifier (emitting anyway would silently over- or under-derive). The supported universal forall v: T where Body, Head (≥2 where-atoms — the last is the consequent, the rest the domain) DOES evaluate: it lowers to the count-equality count { v: Body, Head } == count { v: Body }. OE1315 fires for the shapes that don’t yet: an exists-FOL binder (exists v: T where …), the paren restriction form (forall(path, T)), or a forall whose where clause has fewer than two atoms (ambiguous domain/consequent split). Rephrase via explicit aggregates or an intermediate pub derive; the count { … } / exists { … } subquery forms are unaffected. |
| OE1316 | Error | Live | TemporalOpUnsupported | An unsupported temporal operation reached the evaluator and is refused loudly rather than silently mis-evaluated: arithmetic outside date±duration / date−date / duration±duration (e.g. Date + Date, scaling a temporal value, or mixing a temporal value with a number), a chronological comparison across mismatched temporal types (Date vs Duration, or a temporal value vs a non-temporal one), a calendar-relative duration (P1M/P1Y) or sub-day component (PT…), or today()/now() inside a rule. Surfaced identically in the reasoner and the mutate body. Scalar day-granular date/duration arithmetic and chronological comparison are supported; interval and Allen-relation operations live in the std::time/std::allen library over this value layer. |
| OE1317 | Error | Live | RecursionThroughAggregation | A predicate’s derivation recurses through an aggregate over itself — an aggregate body (count { … }, exists { … }, or the forall count-equality encoding) references a predicate in the same recursive cycle, e.g. pub derive Fulfilled(c) :- …, forall x where x in c.parts, Fulfilled(x);. Stratified-aggregate semantics (Faber-Pfeifer-Leone 2010) require the aggregated predicate to live in a strictly-lower stratum, so recursion through aggregation has no well-defined fixpoint and is refused at ox check / ox build rather than crashing the runtime evaluator. If the aggregate is incidental to the cycle, break it with an intermediate pub derive that materializes the aggregated predicate in a lower stratum. If the aggregate is a universal over the predicate being defined (a forall / count-equality over child parts — recursion depth is data-dependent, so no intermediate derive can leave the cycle), rephrase it as negation-as-failure double negation: derive a counter-example helper with not P(child) (pub derive HasUnfulfilledChild(p) :- childOf(p, c), not Fulfilled(c);), then P(parent) :- …, not HasUnfulfilledChild(parent); — recursion through negation is evaluated under well-founded semantics. |
| OE1318 | Error | Live | UnsupportedMutationForm | A mutate body contains a statement form the runtime cannot yet execute (emit, …). The parser and elaborator admit the form but execution would abort at first invocation, so ox check / ox build refuse the artifact up front instead of shipping a mutation that dies at runtime. Rephrase the statement or wait for the executable form to land. match is executable in BOTH positions: value position (a let RHS, an update … set value, a return/tail value, a require guard, desugared to a Term::IfExpr chain) and statement position (arms running effects, desugared to an Operation::If chain). |
| OE1319 | Error | Live | MatchPatternNotYet | A match arm uses a pattern form that does not yet execute: a binding pattern (a bare identifier that does not resolve to a payloadless enum constant), a payload pattern (Some(x), Type { … }), a tuple pattern, an arm guard, a type-test arm, or an is-outcome arm. The executable subset is ordered first-match over CONSTANT patterns: payloadless enum constant paths (Status::Active), Int / String / Bool / Date literals, or-patterns of those (`A |
| OE1320 | Error | Live | ConditionalArmFieldNotTotal | A rule-body conditional expression (a value if, or a match desugared to one) projects a field inside a conditional ARM that is not total (required) on its holder — the field is declared optional (T?), or no declaration of it is visible. The relational lowering hoists every arm’s projection into an unconditional $field::<f> join, and $field::<f> has no row when an individual lacks the field — so a row missing an optional field would be SILENTLY dropped even when the arm projecting it is never selected. Refused at ox check / ox build rather than mis-evaluated. Workarounds: split the conditional into one rule per arm (each joining only the fields that arm needs), or declare the field required. Projections of required fields inside arms, and projections in always-evaluated positions (the rule body proper, the outermost condition), are unaffected. Lazy presence-guarded branch joins are the tracked long-term fix. |
| OE1321 | Error | Live | EffectInValuePosition | A block in VALUE position (a let RHS block, a value-if branch, a value-match arm) contains an effectful mutate statement (update, delete, forget, require, for, emit). Value lowering reduces a block to its tail expression — the statements would not execute, so the effect would silently vanish. Refused at ox check / ox build instead. Use a statement-position if / match (whose branches DO run effects), or hoist the effect out of the value expression. |
| OE1322 | Error | Live | StaticCheckInstanceVocabulary | A #[static] check reads instance vocabulary. #[static] asserts compile-time discharge intent, and discharge is staged by what the body reads: a check is catalog-level — evaluated finally at ox check / ox build — iff every head parameter and body variable is catalog-sorted: a reflective sort (TypeRef, TraitRef, or Metatype; declaration structure read through specializes, implements, the type column of iof / meta), or an axis-value type (the element type of a pub metaxis Set/Typed domain, read through the catalog-closed $setAxis / $axis relations with n in meta(t).axis). This check binds at least one variable over individuals (a concept/relation predicate atom, a $field:: projection, or the entity column of iof / meta), so its vocabulary classifies it instance-level and it would silently reclassify to runtime discharge — exactly the drift #[static] exists to catch. Either rephrase the body over catalog-sorted variables only, or drop #[static] to accept instance-level (runtime + declared-EDB-at-build) discharge. |
| OE1323 | Error | Live | MalformedCheckDiagnostic | A check rule’s => Diagnostic { … } payload is missing or malformed. Diagnostic is check-surface syntax interpreted by the compiler (like the #[default] defeat-plane directive), not a user type — no import declares it, and its shape is fixed: exactly the fields severity: (the path Severity::Error / Severity::Warning / Severity::Info — nominal, closed set), code: (a string literal), and message: (a string literal, or format!("…{}…", args) with positional {} placeholders matching the argument count one-to-one). code: and message: are always required; severity: is required UNLESS the check is a trait-member impl whose trait pins the member’s severity (check Member(Self) => Severity::…;), in which case omitting severity: inherits the pinned one (a divergent restatement is OE0676); unknown or duplicate fields are refused; at: is reserved for span attribution (the LSP consumes it when it lands) and is refused until then. Named ({name}) or spec’d ({:?}) interpolations are refused — only the empty positional {} form evaluates; a brace placeholder in a plain (non-format!) message literal is also refused, since nothing would interpolate it. Literal braces are written {{ / }} — the escapes work in plain messages and format! templates alike. #[observe] is refused on a non-Error check — Warning / Info checks never guard, so there is nothing to opt out of. |
| OE1324 | Error | Live | CheckCodeNamespace | A check’s code: string does not follow the user-code namespace discipline: the code must be ::-qualified (e.g. "Lease::E001" — a package/domain prefix, then the code), and the OE / OW prefixes are reserved for the compiler’s own catalog (this extends the compiler’s hygiene rule to user space — a user check masquerading as a compiler diagnostic would be indistinguishable in build output and tooling filters). Pick a namespace that names your package or standard ("OntoClean::C1", "HR::W014") and keep the compiler prefixes out of it. |
| OE1325 | Error | Live | CheckMessageArgUnbound | A format! argument in a check’s message: does not resolve against the check body’s bindings. Message arguments are evaluated per violation tuple, so every variable they mention (directly, or as the receiver of a field chain like c.cite) must be bound by the rule — a head parameter or a variable bound by a positive body atom — exactly the range-restriction discipline OE1303 enforces for rule heads. An unbound argument has no value at rendering time and would interpolate garbage (or drop the row) silently, so it is refused at ox check / ox build. Bind the variable with a positive body atom, or interpolate a bound one. |
| OE1326 | Error | Live | TraitMemberNotYet | A trait or impl body declares a form the compiler cannot elaborate. All five TRAIT member forms land end-to-end: rule-plane members (derive/check/query, slice 1) and invocation-plane members (fn/mutate, slice 3) elaborate, conformance-check, and execute. This gate remains for the genuinely unsupported shapes: trait-side DEFAULT member bodies (V1 bounded-generics territory — a default body cannot typecheck against Self’s fields without structural bounds; the OE0667 family), inherent (bare-impl) members (no trait contract, no dispatch story — a follow-on), and unrecognized member forms (associated const/type bodies, junk items). The member parses and is named in this diagnostic together with its declaring trait or impl; ox check / ox build refuse the program rather than shipping an artifact in which the member silently does not exist (v0.1 previously DISCARDED trait/impl body items without a diagnostic). Remove the member, move the default body into each impl, or declare inherent behavior through a trait impl. |
| OE1327 | Error | Live | TraitMemberUncovered | A bare trait-member atom is called over an argument whose static type (the calling rule’s head-parameter annotation) is not FULLY covered by the trait’s impls. Coverage is a workspace-closed catalog scan: every instantiable declared S ⊑ T must satisfy S ⊑ target for some impl target — instantiable means NOT abstract (the type’s own modifier, or its introducing metatype’s, read from the wire — never a user axis name; the pre-S4 sortality token scrape is retired). The default is instantiable: an unmodified declaration obligates coverage, and the exemption is the explicit abstract opt-in. Under naive clause union an uncovered type would make the atom silently FALSE — the silently-vacuous shape this project refuses. Fixes: add the missing impl(s), declare the never-directly-instantiated supertype abstract, or make the partial dispatch explicit with the conformance guard implements(meta(p), Trait) as a body conjunct — the guard’s $implements join then excludes uncovered individuals visibly. A static type with NO covered instantiable subtype stays refused even with the guard written (the guard would be vacuous; the help says so). |
| OE1328 | Error | Live | DuplicateCheckHead | Two check declarations in the same module scope share a head name. derive rules union same-head clauses natively (the §7.3.1 disjunction idiom), but that idiom canNOT apply to checks: a check’s head carries payload identity — the => Diagnostic { severity, code, message } report, the guard classification, and the violation relation all belong to the head — so two checks sharing one head would cross-talk (one check’s violations rendered under the other’s severity, code, and message template, with garbage interpolation when the templates differ). Refused at ox check / ox build. Rename one check, or express the disjunction in a single check body (e.g. over a shared derive predicate whose same-head clauses union, then one check reading it). Trait check members are unaffected: each impl’s monomorphized member rule is qualified by its impl target (Trait::Member@Target) and keeps a distinct head. |
| OE1329 | Error | Live | CheckHeadConsumed | A rule-body predicate atom resolves to a check head. Check heads are observer-only (§7.1: check never populates the IDB) — a check’s violation set is a report with payload identity (severity / code / message), not a derived relation other rules may join. Consuming it would make derived facts depend on diagnostic machinery (and on whether a check happens to be discharged), the inverse of the architecture. Refused at ox check / ox build for every consuming rule mode (derive, check, query, bridge bodies). Derive from the underlying body predicates instead: factor the check’s violation pattern into a pub derive head, reference THAT from both the check and the consuming rule. |
| OE1330 | Error | Live | TemporalQualifierOnWrite | A write statement in a mutate body carries a valid-time qualifier (insert iof(p, T) at <expr>, during <expr>, since <expr>). The parser and elaborator build the qualifier faithfully, but the runtime write path never consumes it — every DML arm destructured it away, so the qualified write applied as if unqualified (the bitemporal extent stamped vt_start = vt_end = None), silently producing atemporal data for a user writing temporal assertions per the book. The storage substrate carries bitemporal extents and as-of reads are exact, but threading user-supplied valid-time through the write path (evaluating the qualifier Term, mapping at/during/since onto vt_start/vt_end, and extending the shared emit_* storage signatures across both backends) is work that is not yet wired. Refused at ox check / ox build rather than dropping the qualifier silently, the same fail-closed discipline as OE1318. Drop the qualifier to write at the current valid time, or wait for the bitemporal mutation forms to land. |
| OE1331 | Error | Live | ValueAggregateBraceForm | A value aggregate (sum / min / max / avg) was written in brace form sum(expr) { atoms }. Only the cardinality aggregates count { … } / exists { … } take the brace form; a value aggregate uses the comprehension form sum(expr for x in Source, atom, …), whose comma-separated trailing atoms (relation joins, comparisons, type tests) refine the fold’s domain and whose grouping is the outer bound variables. Rewrite sum(e.amount) { posted(e, acct) } as sum(e.amount for e in Entry, posted(e, acct)). Refused at ox check / ox build rather than silently mis-folding — one spelling per aggregate role (the house single-spelling rule). |
| OE1332 | Error | Live | AggregateOverUndefined | An aggregate folds over a relation that carries well-founded-UNDEFINED atoms ($undefined::R is non-empty for an aggregated predicate). Aggregates evaluate over the definitely-true extent; folding when part of the input is neither true nor false would silently treat undefined as false (the undefined-as-false leak) — biasing sum / count low and min / max arbitrarily, with no diagnostic. Refused loudly at runtime instead, the same fail-closed discipline the WFS check boundary uses. The refusal is WHOLE-RELATION, not per-group: any non-empty $undefined::R companion trips it, even when the undefined tuples belong to a group key that the current binding would not fold — a conservative stopgap that never folds undefined-as-false. Three-valued aggregate intervals over the undefined set are the designed follow-up; for now, stratify the aggregated predicate so it has a definite extent (no recursion-through-negation feeding the fold), or aggregate over an intermediate pub derive that is itself definite. |
| OE1333 | Error | Live | RoundingBuiltinArity | A rounding builtin (round / round_half_even / trunc) was called with the wrong number of arguments. round(x) rounds to the nearest integer (ties away from zero) and round(x, n) to n decimal places; round_half_even(x, n) rounds to n places with ties to even (banker’s rounding — the money default); trunc(x, n) truncates toward zero at n places. All operate exactly over the numeric tower (Decimal stays Decimal, never via f64). Refused at compile rather than silently dropping the row. |
| OE1334 | Error | Live | BindingLhsNotSimpleVar | A body-level binding atom x = expr has a left-hand side that is not a fresh single-segment variable — a dotted projection (x.f = e) or a ::-qualified path (m::x = e). A binding introduces a NEW variable, so its left-hand side must be a bare identifier. = is the binding (assignment) operator in a rule body, distinct from the comparison ==; it is NOT a field-update operator (that is the mutate-body update … set { field = … } statement). Refused at ox check / ox build rather than falling through to a bare-path Boolean atom, which would silently drop the right-hand expression — the loud-or-working discipline (nothing silently dropped). |
| OE1335 | Error | Live | BindingLhsAlreadyBound | A body-level binding atom x = expr names a left-hand side x that is ALREADY bound — by a prior positive predicate atom, a head parameter, a projection, an aggregate result, or an earlier binding. A binding introduces a FRESH variable; reusing a bound name was silently degrading into an equality FILTER (x joined against the computed value) rather than a binding, so two readings of the same = (bind vs. compare) collapsed with no diagnostic. Refused at ox check / ox build: use == to compare x against the expression, or pick a fresh name for the binding. (== filters over already-bound operands; = binds a new one.) |
| OE1336 | Error | Live | BindingInComprehension | A binding atom x = expr (single =) appears as a trailing atom inside an aggregate comprehension (sum(… for v in S, …, x = expr)). Bindings are NOT a comprehension trailing-atom form: the comprehension body admits predicate joins, comparisons, type tests, and membership — filters that refine the fold’s domain — but a binding introduces a fresh variable whose safety the aggregate sub-scope does not check, so it is refused rather than admitted half-checked. Two rewrites cover every use: project the value directly into the fold (sum(u.v for u in T, rel(t, u)) instead of sum(w for u in T, rel(t, u), w = u.v)), or bind in the OUTER rule body and aggregate the bound variable. Refused at ox check / ox build with this directed hint rather than a generic parse error (== to compare is still accepted as a trailing filter). |
| OE1337 | Error | Live | CalendarRelativeDuration | A duration literal <int>.<unit> names a CALENDAR-RELATIVE unit (.months / .years) whose length in days depends on the anchor date — 1.months is 28–31 days, 1.years is 365 or 366 — so it has no fixed value in Argon’s day-granular Duration layer. The supported units are .days and .weeks (a fixed day count: <n>.days ⤳ n, <n>.weeks ⤳ 7n), which lower at rule-term resolution to a Duration literal that Date ± Duration arithmetic evaluates exactly. Admitting a calendar-relative unit would force a silent, anchor-dependent approximation — refused loudly instead. Use .days / .weeks, or compute the target date explicitly (e.g. an end-of-month date as data). |
| OE1338 | Error | Live | DateIntrinsicArity | A date intrinsic (today()) was called with arguments. The date intrinsics are NULLARY — today() reads the current valid-time of the evaluation (the reasoner’s evaluation clock, fixed for the whole fixpoint so a rule stays a pure function of its snapshot) and takes no parameters. A stray argument is refused at ox check / ox build rather than passed through as an un-evaluable application that would later surface as a generic term-shape failure. Write today(). |
| OE1339 | Error | Live | TemporalQualifierUnexecutable | A mutate-body write’s valid-time qualifier could not be executed. Two cases: (1) during <window> — window-valued valid-time needs an interval value the surface does not yet carry, so it is deferred; use at <date> (valid-from a civil day), which IS executable. (2) the at <date> / since <date> expression did not evaluate to a Date — e.g. a bare arithmetic value where a #YYYY-MM-DD# literal was meant (the #date# silent-Int landmine: a bare 2024-01-01 is integer subtraction, not a date). The valid-time qualifier must evaluate to a Date (a #date# literal or a Date-typed value). Raised at runtime before any event is persisted, so the mutation commits nothing. |
| OE1340 | Error | Live | BareDateArithmetic | A bare YYYY-MM-DD in a rule body parses as INTEGER SUBTRACTION (2024-01-01 ⤳ 2024 - 1 - 1 = 2022), not a calendar date, and was silently accepted — even against a Date-typed position — modeling a date off by two millennia with no diagnostic (the #date# silent-Int landmine). Argon’s calendar-date literal is HASH-DELIMITED: write #YYYY-MM-DD#. The check fires only on the exact date shape — three zero-padded integer literals (4 / 2 / 2 digit widths) whose fields form a valid civil date (year 1000–9999, month 01–12, day 01–31) — so a genuine subtraction of differently-shaped operands is never flagged. Refused at ox check / ox build with a directed hint toward the real literal. |
| OE1341 | Error | Live | RelationCardinalityExceeded | A mutate-body relation write (insert <rel>(…)) would exceed a relation endpoint’s declared finite max-cardinality (§5). The i-th bracket constrains how many DISTINCT values position i may take for a fixed combination of the OTHER positions — the standard UML association-end multiplicity. So in pub rel ParentOf(parent: Person, child: Person) [0..2] [*], the position-0 bracket [0..2] caps each fixed CHILD at two distinct parents; a third distinct parent for one child refuses atomically (the body commits nothing) under the closed-world default, counted over committed-plus-overlay tuples (read-your-writes). Before this gate the bracket parsed into a token-soup node no lowering read and the cap was silently ignored. Max-caps are the CWA-checkable half of a cardinality bracket; the min-card half is the deferred OW1342. Emitted by the runtime write path; an open upper bound ([lo..*] / [*]) imposes no cap. |
| OE1342 | Error | Live | BodilessDeriveUnboundArg | A bodiless pub derive P(...) — a derive head with no :- ... clause — is a GROUND FACT: it seeds P’s extent with the named tuple, and every head argument must therefore be a CONCRETE term (a declared individual in scope, an enum constant, an axis value, or a type reference). The named argument is none of these — it is a free variable, and a bodiless derive has no body to range-restrict it, so the tuple it would seed is undefined. (This is the bodiless-derive analogue of the range-restriction refusal OE1303 for rules WITH a body: there a head variable is bound by a positive body atom; here there is no body, so the argument must be a constant.) Declare the individual (pub fact T(name); classifies name into concept T), or add a :- ... body that binds the variable. To seed ground tuples on a derived predicate, this bodiless form over concrete arguments is the sanctioned idiom — it unions with P’s other derive clauses and rules over the same head; alongside it, declaring pub rel P(...) and asserting pub fact P(...) is equally valid. |
| OE1343 | Error | Live | MalformedCardinality | A relation endpoint cardinality bracket is malformed (§5): the lower bound exceeds the upper ([3..1]), a bound is non-numeric, or the bracket contains junk tokens. Before this the bracket parsed into a token-soup node that no lowering read, so a malformed cardinality was silently dropped along with a well-formed one; now the bracket is interpreted into a structured cardinality and a malformed one is refused at ox check / ox build. Use [n] (exactly n), [lo..hi], [lo..*] (at least lo), or [*] (any). |
| OE1344 | Error | Live | QueryCteNotYetImplemented | A query select-body opens a with <name> = select …; common-table-expression (CTE) clause (§7.4) that does not yet execute. The CTE surface is documented but the binding is not wired into the query planner, so accepting one silently would drop the named sub-result and evaluate the outer select against nothing. Refused by name at parse — distinct from a generic typo — until the planner materializes CTE bindings. For now, inline the sub-query’s filter into the outer select … from … where …, or declare an intermediate pub derive and select over it. |
| OE1345 | Error | Live | QueryGraphTraversalNotYetImplemented | A query uses a graph-/path-traversal form (§7.4) that does not yet execute: a select shortest path … / select all_shortest path … projection, a select path <name> … path-binding projection, or a `… as |
| OE1346 | Error | Live | QueryOptionalFromNotYetImplemented | A query select-body uses an optional from … clause (§7.4) — the left-outer-join form that admits rows whose optional pattern has no match (binding its variables to null) — which does not yet execute. The clause is documented but the planner has no outer-join operator, so accepting one silently would drop exactly the rows it is meant to keep. Refused by name at parse until the outer-join lands. Split into two queries, or model the optional fact with an explicit Option-typed field, for now. |
| OE1347 | Error | Live | QuerySetOpNotYetImplemented | A query combines two select-bodies with a result-table set operator — union, union all, intersect, or except (§7.4) — that does not yet execute. The operators lex and the surface is documented, but the planner has no set-combination stage, so accepting one silently would return only the first select’s rows. Refused by name at parse rather than a generic token error. Combine the branches with multiple pub derive clauses sharing one head (union), or filter explicitly (intersect/except), for now. |
| OE1348 | Error | Live | RuleRoleStepClosureNotYetImplemented | A rule body uses a role-step transitive-closure navigation — x.Role+(y: T) (one-or-more steps) or x.Role*(y: T) (zero-or-more) (§7.3.1) — that does not yet execute. The closure surface is documented (the headline ontology path-traversal form) but the reasoner has no closure operator on a role step, so the +/* after a navigation parsed as stray arithmetic and died generic. Refused by name at parse until role-step closure lands. Write the closure as an explicit recursive pub derive reaches(x, y) :- x.Role(y); reaches(x, y) :- x.Role(z), reaches(z, y); for now. |
| OE1349 | Error | Live | RuleOutcomeSuffixNotYetImplemented | A rule-body predicate atom carries a reasoning-outcome suffix — is both(a, b), is ambiguous(a), or is timeout(a) (§7.3.1 / §14) — that does not yet execute. The is unknown outcome IS executable (it reads the well-founded undefined value); the multi-valued outcomes (both = paraconsistent over-determination, ambiguous/timeout = SLG resolution states) need the richer §14 reasoning-outcome surface. Before this, is ambiguous(a) / is timeout(a) parsed and were SILENTLY DISCARDED (lowered to the bare predicate, dropping the outcome) and is both(a, b) died on a generic parse error — both now refuse by name. Use is unknown for the WFS-undefined case for now. |
| OE1350 | Error | Live | RuleTemporalAtomNotYetImplemented | A rule body leads with a metric-temporal atom — ever, always, since, until, during, box_minus, diamond_minus, box_plus, diamond_plus (§7.3.2) — that does not yet execute. The DatalogMTL operator surface is documented and the Lean substrate models it, but the Rust reasoner has no temporal-fixpoint evaluator, so these keywords had no rule-atom production and fell through to a generic parse error. Refused by name at parse until the temporal reasoner lands. Model time explicitly with Date-typed fields and ordinary comparisons for now. |
| OE1351 | Error | Live | UnsafeLogicBlockNotYetImplemented | An unsafe logic { … } block (§9.3) — the escape hatch that admits a full first-order-logic formula above the decidable tier ladder, opting out of the tier classifier’s termination guarantee — does not yet execute. The surface is documented for the fol tier but no FOL solver is wired in, so the block had no item production and died at module level with a generic parse error. Refused by name until the FOL escape hatch lands. Stay within the decidable tiers (structural…recursive) for now. |
| OE1352 | Error | Live | MutateUpsertNotYetImplemented | A mutate body uses an upsert statement (§8) — insert-or-update keyed on identity, with optional on insert { … } on update { … } arms — that does not yet execute. The keyword is reserved and the surface is documented, but the write path has no upsert effect, so upsert had no statement production and fell through to a generic expression parse error. Refused by name at parse until upsert lands. Branch explicitly on existence with require + a conditional insert / update for now. |
| OE1353 | Error | Live | MutateDetachDeleteNotYetImplemented | A mutate body uses a detach delete <rel>(…) statement (§8) — delete a relation tuple and the individuals it solely connects — that does not yet execute. The detach keyword is reserved and the surface is documented, but the write path has no detaching-delete effect (only a plain delete tuple/iof), so detach had no statement production and died on a generic parse error. Refused by name at parse until detach-delete lands. Use a plain delete <rel>(…) and delete any orphaned individuals explicitly for now. |
| OE1354 | Error | Live | AsyncFnNotYetImplemented | A function is declared async (or uses .await) (Appendix B) — asynchronous evaluation — which does not yet execute. The async / await keywords are reserved against a future effect system but there is no async runtime, so async fn had no declaration production and fell through to a generic module-level parse error. Refused by name at parse until async lands. Declare a plain pub fn for now. |
| OE1355 | Error | Live | ConstDeclNotYetImplemented | A top-level const <NAME>: <T> = <expr>; declaration (§19) does not yet execute. The form parsed but SILENTLY SWALLOWED its initializer — it emitted no event and bound no value, so a model could reference a constant that did not exist with no diagnostic (contradicting the no-hollow-features rule and STATUS’s every surface decl lowers to events). Refused by name at parse, rather than silently accepted, until const evaluation and the compile-time-constant plane land. Inline the literal at each use site, or model it as a field default, for now. |
| OE1356 | Error | Live | SinkDeclNotYetImplemented | A top-level pub sink <name> { … } declaration (§7.5) does not yet execute. The form parsed but SILENTLY SWALLOWED its brace body via balanced-token skipping — it emitted no event and validated nothing inside the braces, so arbitrary garbage between the braces was accepted with no diagnostic (contradicting the no-hollow-features rule). The companion emit statement is already refused loudly (OE1318); the sink DECLARATION now refuses by name too rather than swallowing, until sink publication has a runtime. A bare pub sink <name>: <Type>; declaration form (no body) is the documented typed-sink shape and is unaffected. |
| OE1357 | Error | Live | RuleNegatedSomePayloadBindingUnsupported | A rule body negates an Option/T? payload binding — path is not Some(<ident>). The positive form is Some(a) binds a to the payload and the conjunction fails on None; under negation there is no payload to bind (the binder would range over nothing), so a bound is not Some(a) is incoherent. The intended meaning is the absence test, written path is None (true iff the field is absent) — refused by name rather than binding a meaningless variable. Use path is None for the negative test, or path is Some(a), not <p>(a) to negate a property of the payload. |
| OE1358 | Error | Live | RuleOptionTestNotFieldProjection | An Option/T? membership test is Some(a) / is None has a left side that is not a field projection. The test reads an OPTIONAL FIELD’s presence — a present field materializes one $field::<f>(holder, value) row, an absent one materializes none — so its left side must be a field access (p.age is Some(a)), not a bare variable or an expression with no relationalizable receiver. Refused by name rather than silently dropping the option test. Write <holder>.<field> is Some(a) / <holder>.<field> is None. |
| OE1359 | Error | Live | RelationIrreflexivityViolated | A relation declared #[irreflexive] (a generic relation-algebra property — no element relates to itself, OWL IrreflexiveObjectProperty) has a self-loop R(x, x) in its extent. #[irreflexive] synthesizes a check rule over the declared relation’s own extent that fires on any tuple whose two positions are equal; this is that check firing. Either the offending self-pair must not be asserted, or the relation is not actually irreflexive and the directive should be removed. (The directive enforces the relation’s OWN extent — propagation to relations classified-by an irreflexive metarel is a recorded follow-up, not yet enforced.) |
| OE1360 | Error | Live | RelationAsymmetryViolated | A relation declared #[asymmetric] (a generic relation-algebra property — R(x, y) forbids R(y, x), OWL AsymmetricObjectProperty) contains a mutual pair: both R(x, y) and R(y, x) are in its extent. #[asymmetric] synthesizes a check rule over the declared relation’s own extent that fires when a tuple and its converse both hold; this is that check firing. Either one direction of the mutual pair must not be asserted, or the relation is not actually asymmetric and the directive should be removed. Note asymmetry implies irreflexivity (a self-loop is its own converse), so #[asymmetric] alone already forbids R(x, x). |
| OE1361 | Error | Live | RelationFunctionalityViolated | A relation declared #[functional] (a generic relation-algebra property — each source maps to at most one target, OWL FunctionalObjectProperty) has a source related to two distinct targets. #[functional] reuses the relation max-cardinality machinery: it synthesizes a [0..1] upper bound on the relation’s TARGET (last) position, which the write path enforces atomically — a second distinct target for an already-mapped source is refused before it commits (the same mechanism as OE1341). This code names the functional-property origin of that refusal so the modeler sees that a #[functional] directive, not a hand-written [0..1] bracket, is the constraint being enforced. |
| OE1362 | Error | Reserved | RelationPropertyNotImported | Retired — no longer emitted. #[functional] re-homed from a #[builtin] stub to a genuine procedural macro in std::rel (it re-emits a rel with a [0..1] cardinality cap on the target endpoint via structural reflection, or pastes a guarding check on a metarel). It is no longer a registered directive, so an unimported #[functional] — like its siblings #[transitive]/#[irreflexive]/#[asymmetric] — is now refused as OE0705 (unknown attribute macro). This code is retained for historical numbering. |
| OE1363 | Error | Live | MutationCallCycle | The mutation call graph contains a cycle — a mutate body invokes another mutation (directly, mutually, or itself) along a path that returns to its start (§7.5). A sub-mutation call is closed-nested: it runs the callee’s operations into the CALLER’s transactional buffer rather than evaluating to a fixpoint, so the whole transitive call tree is one atomic transaction that commits or aborts wholesale. A recursive cycle therefore has no terminating composed evaluation in v1 — it would unbound-recurse the runtime — so the call graph is required to be statically acyclic and the cycle is refused at ox check / ox build, naming the trail, rather than shipping a mutation that overflows the stack on first invocation (the same fail-closed discipline as OE1318). A trait-member callable edges to every impl body in its group (dispatch picks one at runtime, but any is statically reachable). Break the cycle; recursive mutation composition (fixpoint-to-quiescence) is a separately gated future feature. |
| OE1364 | Error | Live | UnknownTermInBody | A derive / query / check / fn body contains an expression the elaborator could not lower — it degraded to the <unknown> lowering sentinel (Term::Var { name: "<unknown>" }), the fallback an un-lowerable expression form falls back to during expr/atom lowering. The sentinel resolves to no binding at runtime and surfaces as a generic “unbound variable” the first time the body is evaluated, so a body reaching a built artifact carrying one is a silent landmine (this is the same bug-class OE1318 closes for mutate bodies — the self-in-expression-position case — extended to every rule-plane and compute body). The sentinel is refused at ox check / ox build, naming the offending declaration, rather than shipping a rule/query/check/fn that dies generically on its first evaluation. Rephrase the offending expression into a supported form. |
| OE1365 | Error | Live | NegatedWfsRelation | A derive rule negates (not R(..)) a relation R that is evaluated under well-founded semantics in a DIFFERENT stratum — a relation whose extent can carry well-founded-UNDEFINED atoms because recursion-through-negation left part of it neither true nor false. The well-founded SCC materializes 2-valued at its boundary: only the definitely-true extent lands in R; the undefined tuples live in the $undefined::R companion relation. Ordinary derive-rule NAF reads only R, so a higher stratum’s not R(x) for a genuinely-UNDEFINED R(x) would read it as definitely-FALSE and over-assert the head — an unsound over-derivation with no diagnostic. Refused LOUDLY at ox check / ox build rather than silently evaluating undefined-as-false, the same fail-closed discipline OE1332 uses for aggregates over an undefined relation (the check boundary already excludes $undefined::R correctly via its K3 firing rule; derive heads do not yet). This refuses ONLY the genuinely-unsound case — a NAF edge whose target SCC is well-founded and is NOT the negating rule’s own SCC; ordinary stratified NAF over a fully 2-valued relation (no recursion-through-negation) is unaffected, and a same-SCC NAF is the recursion-through-negation the well-founded evaluator handles internally. Collapse the negation into the well-founded SCC: instead of splitting into a lower R stratum and a higher H :- .., not R(..) stratum, route H’s recursion through its OWN negation so {H, R} form a single NAF-cyclic SCC the engine evaluates by well-founded semantics directly (the documented win-move idiom in examples/robot_plan_execution/robot.ar ~115-140). Three-valued propagation of UNDEFINED into derive heads (the AFT pair-encoding) is tracked separately. |
| OE1366 | Error | Live | CheckViolation | A user-authored check rule fired at Severity::Error (static discharge). The check’s body matched over the package’s catalog / declared EDB, so the obligation it encodes is violated. This is the typed-channel carrier the language server uses to surface a fired check in-editor: the violation’s own namespaced code (e.g. Lease::E001) and its rendered message lead the diagnostic message, since a user check’s identity is the author’s ::-qualified string, not a compiler catalog code. ox check / ox build report the same violation through the user-diagnostic channel and fail the command (the loud-gate); #[observe] opts an Error check out of blocking, and Warning / Info checks render and pass — neither is carried through this code. |
| OE1367 | Error | Live | NegatedOpenWorldDerived | A rule negates (not H(..)) a relation H whose extent can carry OPEN-WORLD-UNDEFINED atoms — H is (transitively) derived through a negation over an open-world (#[world(open)] / default_world = "open") relation, yet H itself is read closed-world. An open-world not C(x) over an absent C(x) is can (unknown), not a definite not, so H’s definitely-true extent omits those can rows; a 2-valued not H(x) then reads such a can row as definitely-FALSE and over-asserts the head — an unsound over-derivation with no diagnostic. Refused LOUDLY at ox check / ox build rather than silently evaluating open-world-undefined-as-false — the open-world analog of OE1365 (the well-founded case), the same fail-closed discipline. This refuses ONLY the genuinely-unsound case: a closed-world not H over a relation whose derivation depends on an open-world negation. To resolve: mark H’s concept #[world(open)] too (so not H reads open-world and tolerates the can), or restructure so the open-world negation does not flow into a closed-world-negated head. Full three-valued propagation of open-world-undefined into derive heads (the AFT pair-encoding) is tracked uniformly with the well-founded case. |
| OW1342 | Warning | Live | RelationMinCardinalityDeferred | A relation endpoint declares a non-zero MINIMUM cardinality (pub rel Married(a: Person, b: Person) [1..1] — every person must be married) that the closed-world write/check channel cannot soundly enforce in v0. Under CWA a build-time refusal would wrongly reject staged construction (relate the entity in a later mutation), and the field-access-time / evaluation-channel emitter that would actively check it is designed but not built in v0 (the same disposition as OE0207’s staged-construction note). The bracket is NOT silently accepted-as-enforced: it is parsed into a structured cardinality, PERSISTED on the wire (RelationDeclBody.cardinalities), the max-cap half IS enforced at the write path (OE1341), and this WARNING is emitted at ox check / ox build (by oxc-driver) naming the relation + position so the modeler knows the minimum is recorded-not-enforced. Full min-card-under-OWA enforcement is a recorded follow-on. |
Cross-level instantiation bodies (19xx)
Field-coverage gates for cross-level instantiation bodies (§5.2). The MLT reasoning codes (categorization / partition / order / subordination / powertype) that once sat here are gone — MLT is an unprivileged package (packages/mlt), so those constraints are authored as the package’s own rules/checks over the neutral substrate, not as reserved compiler codes.
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OE1901 | Error | Reserved | InstantiationFieldNotInTarget | MLT cross-level body assigns = to a field absent from the target metatype (§5.2). |
| OE1902 | Error | Reserved | InstantiationFieldShadows | MLT cross-level body declares a : field that shadows an inherited field (§5.2). |
| OE1908 | Error | Reserved | IntrinsicPropertyMissing | MLT body fails to bind a #[intrinsic] field required by the target metatype (§8). |
Collection operators (24xx)
| Code | Severity | Status | Name | Meaning |
|---|---|---|---|---|
| OW2402 | Warning | Reserved | RefineSingletonAsOption | A refinement [T; <=1] is more naturally expressed as T? (Option<T>) (§5.3). |
Explanations
Codes with an extended explanation (also available offline via ox explain <CODE>):
OE0204 — UnknownField
Field access x.f where the receiver’s established type declares no field f
(counting inherited fields and any : T narrowing in scope). In a rule body the
error fires only on provable absence: an Argon individual can be classified
under several concepts, and a field on any established concept is admitted, so
f must be missing from every one of them. A receiver with no established
concept (an unbound variable, or a path to a declared type/enum) is skipped.
Fix: declare the field on the concept, narrow the receiver to a type that has it, or correct the field name.
use std::core::{type, rel};
pub type Person { name: String, age: Nat }
pub derive adult(p) :- p: Person, p.age >= 18; // ok: `age` is on Person
pub derive bad(p) :- p: Person, p.income > 0; // OE0204: no `income` on Person
OE0211 — IofInsertOnDefined
A concept defined with iff is defined, not asserted: its membership is
derived from the refinement predicate, so you cannot insert iof into
it — the substrate classifies individuals automatically whenever the predicate
holds. (A primitive concept declared with where is the opposite: membership is
asserted, and insert iof is exactly how you assert it.)
Fix — write the underlying state the definition reads, and let classification follow:
use std::core::{type, rel};
pub type Adult <: Person iff { self.age >= 18 };
pub mutate seed() {
insert Person { name: "Mia", age: 19 }; // not `insert iof Adult`
// Mia classifies as Adult automatically (age >= 18).
}
The same applies through <:-ancestors: if any defined ancestor derives the
membership, the insert iof is refused.
OE0213 — IofInsertIntoBot
Bot (⊥) is the empty concept — the lattice bottom. By definition it has no
instances: nothing is ever an instance of Bot. So insert iof(x, Bot) asserts
a contradiction (x is a member of a category that, by definition, is empty) and
is refused statically.
This is a sound, unconditional refusal — it follows from Bot’s definition
alone, not from any two-concept disjointness analysis (that stays reserved). The
dual, Top (⊤), is the opposite: every individual is an instance of it, so a
rule body Top(x) ranges over the whole domain.
If you meant to assert membership in a real concept, name that concept:
pub type Person;
pub mutate seed(p: Person) {
insert iof(p, Person); // not `insert iof(p, Bot)`
}
OE0605 — UnknownMetatype
Argon has no built-in ontology vocabulary. A declaration like pub kind Person
reads kind as a classifier, and classifiers are not keywords — the leading
introducer must resolve to a pub metatype that is in scope (§3.4). That is what
keeps the language ontology-neutral: kind, class, rigid_sortal and friends
ship in vocabulary packages (UFO, BFO, …), never in the compiler.
Even the no-commitment baseline type/rel are not ambient: they
live in std::core and must be imported.
Fix — bring the baseline into scope:
use std::core::{type, rel};
pub type Person; // ontology-uncommitted
or commit to a vocabulary you declare or import:
pub metatype kind = { }; // declares the `kind` classifier
pub kind Person; // now legal
To make the baseline available across a whole package, set
prelude = ["std::core::{type, rel}"] under [package] in ox.toml.
OE0668 — RefinementInvariantViolated
A primitive concept declared with where carries a necessary invariant: every
member must satisfy it. This write would place an individual in the concept while
the predicate evaluates to a definite false, so it is refused.
Three-valued by design: only a definite false blocks the write. If the
predicate is unknown (a field the data has not supplied yet), the write is
allowed — absence of information is not a violation.
use std::core::{type, rel};
pub type Adult <: Person where { self.age >= 18 };
pub mutate admit() {
insert Adult { name: "Lee", age: 15 }; // OE0668: 15 >= 18 is definitely false
insert Adult { name: "Ada", age: 21 }; // ok
// age unknown -> allowed (unknown, not false)
}
Fix: write a value that satisfies the invariant, model the looser set with a
weaker where, or — if membership should be derived rather than asserted — use
an iff definition instead.
OE0239 — FactReferencesDerivedPredicate
A predicate in Argon has a single origin: its extent is either EXTENSIONAL
(asserted as ground facts) or INTENSIONAL (computed by derive/query rules) —
never both. pub fact P(...) asserts into an extensional relation, but here P
is a rule head, so the seed fact would key a different relation node than the
rule reads and silently drop out of the fixpoint: no error, wrong answer. Argon
refuses at ox check / ox build rather than emit an artifact that mis-derives.
To give a derived predicate ground tuples, keep it single-origin: seed the head
with a bodiless derive clause, or read a base relation. Note that rule bodies
must be range-restricted — bind each head variable with a positive atom, not a
bare comparison (p.income > 0 alone leaves p unbound):
use std::core::{type, rel};
// a base relation the rule reads (seed it with `pub fact HighEarner(...)`),
// unioned with the rule clauses over the same head:
pub rel HighEarner(Person);
pub derive owes_tax(p) :- HighEarner(p);
pub derive owes_tax(p) :- p: Person, p.income > 0;
Axiom-ADT variant catalog
The canonical axiom_events table (The single-event-log architecture) carries one of 26 axiom-kind variants. Each variant has a stable string tag (the axiom_kind column value) and a canonical-CBOR body shape (the body BYTEA payload).
The catalog is ontology-neutral. UFO contributes zero variants; UFO-specific meta-properties (rigidity, sortality, identity-provision) are meta_property events whose body declares (axis, target, value). Other vocabulary packages (BFO, DOLCE, MLT) contribute zero variants on the same principle.
The variants mirror Argon’s CoreIR 1:1. The canonical source is the Lean inductive Argon/Storage/AxiomKind.lean (variant set and order) and Argon/Storage/AxiomBody.lean (per-variant body shapes); the Rust mirror is oxc-protocol::storage::AxiomKind, pinned against drift by oxc-protocol/tests/drift.rs. When CoreIR shakes during compiler development, the Lean drives and this catalog follows. For the exact CBOR field shape of any variant, read AxiomBody.lean — it is authoritative; the body shapes are not reproduced here.
The 26 variants
Tag (axiom_kind) | Group | Notes |
|---|---|---|
concept_decl | Declarations (11) | Ontology-neutral; UFO axes via meta_property |
relation_decl | is_iof / is_subsumption flags identify privileged relations | |
struct_decl | ADT-product type | |
enum_decl | ADT-sum type | |
metaxis_decl | A meta-axis exists; vocab packages ship their axes here | |
metatype_decl | Bundles axis-value commitments for a concept-introducer | |
metarel_decl | Structural mirror of metatype_decl, for relations | |
standpoint_decl | Multi-parent DAG; rigidity per node | |
module_decl | Carries the composition signature (Composition signature) | |
trait_decl | Trait surface | |
impl_decl | Conditional impls supported | |
iof_assertion | Assertions (7) | “individual is an instance of concept”; bitemporal-aware |
iof_refutation | Strong (classical) negation of an iof_assertion (RFD 0010); maps to Truth4::Not at federation; same body as iof_assertion | |
relation_tuple | n-ary relation tuple; literal codomains allowed | |
relation_tuple_refutation | Strong-negation counterpart of relation_tuple (RFD 0010); same body as relation_tuple | |
property_assertion | Literal-valued specialization of relation_tuple; declared-symbol carrier | |
individual_property_assertion | Per-instance mut-field set (RFD 0006); per-instance IndividualId carrier | |
meta_property | Vocab-specific axis values land here, not in catalog columns | |
subsumption_axiom | Axioms (2) | sub <: super edge; multi-parent via multiple events |
partition_axiom | Vocab-neutral cover; MLT’s partitions(T) lowers onto this | |
rule_decl | Rule-modes (4) | Unifies fn / derive / query / mutate / check via a rule_mode field |
query_decl | Distinct from rule_decl to index queries; query-specific attrs (group_by, order_by, limit, offset) | |
mutation_decl | Named pub mutate; operations include forget (capability-gated, Capability surface) | |
compute_decl | Inline-compute (tier: recursive by definition) | |
bridge_decl | Federation (1) | Bridge rules bridge rule — directional inference between standpoints; added in δ.2 |
test_decl | Tests (1) | testing / The test atom — ox test in-language unit test (RFD 0048); body is a mutate-body Vec<Operation> plus Operation::Assert, run by ox test. Inert at query/serve time — carries no rule-mode semantics |
Total: 26. The body encoding is canonical CBOR (deterministic per RFC 8949 §4.2.1); content_id = SHA-256(body), so identical bodies deduplicate. UUIDs encode as bytes(16). The per-variant body maps are specified in Argon/Storage/AxiomBody.lean.
The set is MVP-final but not closure-locked. Additive variant additions bump core_ir_version minor; breaking changes bump major.
A .oxbin reader that encounters an unknown axiom_kind in the events section MUST report OE1220 UnknownAxiomKind(kind) and refuse the load — unless the artifact is lenient-validated (The OxbinRuntime trait surface), in which case the unknown events are dropped from the in-memory representation but kept on disk. Strict runtimes always fail.
Runtime error codes
Failures on the ox runtime serve /v1 HTTP surface (Serving surface) carry a stable
error.code string in the ARGON_RUNTIME_* family. This is the published
failure reference an integrator switches on. Every 4xx/5xx response —
including routing, method, and request-body parse failures — uses the same
envelope, so a client can key on error.code uniformly:
{
"error": { "code": "ARGON_RUNTIME_VALIDATION_FAILED", "message": "…", "details": { … } },
"requestId": "…",
"moduleHash": "…"
}
These are runtime codes — distinct from the compile-time OE#### catalog
of Appendix C, which are build-time
diagnostics. Runtime codes are an API contract surfaced at request time.
The set below is checked against the runtime’s canonical registry
(RUNTIME_ERROR_CODES in oxc-serve): a drift test in oxc-serve fails if a
code is emitted without a row here, or a row names a code the runtime does not
emit. Do not hand-add a row without the matching registry entry.
| Code | HTTP | Meaning |
|---|---|---|
ARGON_RUNTIME_VALIDATION_FAILED | 400 | Argument coercion, entity-reference validation, or a malformed / wrong-content-type request body failed. |
ARGON_RUNTIME_SIGNATURE_MISMATCH | 400 | Descriptor arguments do not match the declared pub callable’s parameters. |
ARGON_RUNTIME_MISSING_CONTEXT | 400 | A required context field (e.g. tenantId) was absent from headers and body. |
ARGON_RUNTIME_FORK_CONFLICT | 400 | The x-fork-id header and the request/path fork disagree. |
ARGON_RUNTIME_INVALID_CURSOR | 400 | A pagination cursor was not issued by this server — pass back nextCursor verbatim or omit it. |
ARGON_RUNTIME_UNSUPPORTED_QUERY_BODY | 400 | The declared query’s body uses a construct the served evaluator does not yet support. |
ARGON_RUNTIME_UNSUPPORTED_QUERY_RESULT | 400 | The query’s result shape cannot be rendered on the wire. |
ARGON_RUNTIME_UNSUPPORTED_MUTATION_RESULT | 400 | The mutation’s result shape cannot be rendered on the wire. |
ARGON_RUNTIME_DERIVE_FAILED | 400 | A derive / projection evaluation failed for the named rule. |
ARGON_RUNTIME_EXPLAIN_FAILED | 400 | RFD 0046 D3: reconstructing the per-fact proof tree failed for the named relation (e.g. the relation could not be resolved or the materialization erred). |
ARGON_RUNTIME_EXPLAIN_BAD_TUPLE | 400 | RFD 0046 D3: an element of the explain request’s tuple is not a valid wire value. |
ARGON_RUNTIME_CHECK_VIOLATION | 400 | An RFD 0025 delta-guard rejection: the mutation would create a blocking check violation. Atomic — nothing persisted; error.details.diagnostics lists the firings. |
ARGON_RUNTIME_ADHOC_TYPE_ERROR | 400 | An ad-hoc query/mutation body failed to type-check against the loaded module’s schema; error.details.diagnostics lists the firings. The body is never run. |
ARGON_RUNTIME_ADHOC_PREPARATION_FAILED | 400 | An ad-hoc query/mutation source could not be parsed/lowered to an executable form (malformed source, no query/derive/mutate item, or a lowering refusal). |
ARGON_RUNTIME_FORK_NOT_DERIVED | 400 | A fork operation needs a prior derive that has not run. |
ARGON_RUNTIME_METHOD_NOT_ALLOWED | 405 | The path exists but does not support the request method. |
ARGON_RUNTIME_CAPABILITY_DENIED | 403 | The request needs a runtime capability (e.g. fork, forget) the caller/server does not grant. |
ARGON_RUNTIME_ADHOC_DISABLED | 403 | The ad-hoc query/mutation surface is disabled on this deployment (RFD 0033): it is restricted to its declared invocables. |
ARGON_RUNTIME_UNKNOWN_ROUTE | 404 | No route matches the request path. |
ARGON_RUNTIME_UNKNOWN_QUERY | 404 | No declared pub query matches the descriptor’s qualified path. |
ARGON_RUNTIME_UNKNOWN_MUTATION | 404 | No declared pub mutate matches the descriptor’s qualified path. |
ARGON_RUNTIME_UNKNOWN_COMPUTE | 404 | No supported pure pub fn matches the compute descriptor’s qualified path. |
ARGON_RUNTIME_UNKNOWN_CHECK | 404 | The /v1/checks check filter names no check in the module. |
ARGON_RUNTIME_FORK_NOT_FOUND | 404 | The named fork does not exist in this scope. |
ARGON_RUNTIME_REQUEST_TIMEOUT | 408 | The request (or the synchronous reasoner) exceeded its wall-clock budget and was abandoned before persisting. |
ARGON_RUNTIME_FORK_CLOSED | 409 | The fork has been promoted or aborted and is no longer writable. |
ARGON_RUNTIME_SCOPE_ID_COLLISION | 409 | Two distinct scopes hash to the same scope id (a configuration fault). |
ARGON_RUNTIME_SCHEMA_MISMATCH | 409 | An artifact’s schema identity does not match the scope’s recorded identity (R-B7); set ARGON_ACCEPT_SCHEMA_CHANGE=1 to override. |
ARGON_RUNTIME_MODULE_INCOMPATIBLE | 409 | A hot-reload candidate is incompatible with live ABox state (removed / changed declarations). |
ARGON_RUNTIME_ADHOC_MULTI_MODULE_UNSUPPORTED | 409 | An ad-hoc query/mutation was submitted against a composed/multi-module artifact (declarations span more than one module prefix); a bare body predicate has no single prefix to re-qualify against. A tracked follow-on (RFD 0033, open questions). |
ARGON_RUNTIME_RESULT_TOO_LARGE | 413 | An un-paged read exceeded the result-row cap; narrow the request or page it (page / ?limit). |
ARGON_RUNTIME_REQUEST_TOO_LARGE | 413 | The request body exceeded the server’s body-size limit (default 4 MiB). |
ARGON_RUNTIME_CHECK_EVAL_FAILED | 500 | A check could not be evaluated (a server / artifact fault, not a client error). |
ARGON_RUNTIME_STORAGE_ERROR | 500 | The storage backend returned an error during the operation. |
ARGON_RUNTIME_MODULE_NOT_LOADED | 503 | No module is currently loaded (a failed load left the prior module active or none present). |
ARGON_RUNTIME_STORAGE_UNREACHABLE | 503 | /readyz / /v1/health: the storage backend is not reachable. |
A batch-step failure (POST /v1/batch) rides the same envelope under the
step’s own code, with the failing step’s 0-based index added at
error.details.batchStep. A 429/load-shedding code is not part of the
current surface (the request-timeout and result-row caps are the back-pressure
mechanisms).
Mechanization provenance
This index maps each language feature to the Lean module(s) that mechanize it and the spec section it lives in. It is generated from the feature registry, and CI verifies that every cited module exists — a feature naming a vanished mechanization fails the build.
| Feature | § | Lean module(s) | RFD |
|---|---|---|---|
| Module extraction (⊥-locality) | §3 | Locality/Locality.leanLocality/ScopedConservativity.lean | — |
mod, use, visibility, multi-file build, ox.toml package | §3 | CoreIR/Path.lean | 0022 |
| The meta-calculus (metaxis / metatype / metarel) | §4 | Substrate/MetaCalculus.lean | — |
abstract / fixed declaration modifiers | §4 | Runtime/ModifierGates.lean | 0027 |
Concepts (type baseline; declared classifying vocabulary) | §5 | — | 0038 |
Facts pub fact / strong negation pub not_fact | §5 | Storage/AxiomEvent.lean | 0010 |
| First-class relations | §5 | Substrate/RelationSubsumption.lean | 0005 |
Relation subsumption R2 <: R1 | §5 | TypeSystem/RelationExtension.lean | 0005 |
struct / enum data declarations | §5 | Substrate/Construct.lean | — |
Field intents T / T? / Truth4Of<T> | §6 | Foundation/Truth4Of.lean | 0007 |
Generics, collections ([T] / Set / Map / Option), function types | §6 | Syntax/TypeExpr.lean | — |
Instance-of : clause (cross-level) | §6 | MetaCalculus/IsCanNot.lean | — |
Mutable fields mut + update | §6 | Runtime/MutationSemantics.lean | 0006 |
Numeric tower — Top/Bot, exact Decimal/Real, result widening | §6 | Runtime/AggregateExact.lean | 0016 |
Refinement: iff (defined) vs where (primitive) | §6 | TypeSystem/Realization.lean | 0017 |
Specialization <: (subtyping) and a concept hierarchy | §6 | TypeSystem/Subtyping.lean | — |
| World assumptions — CWA default + per-concept OWA opt-in (`#[world(open | closed)]`) | §6 | Schema/WorldAssumption.leanTypeSystem/Soundness/CwaOwa.lean |
Aggregates count / exists / sum / min / max / avg | §7 | Runtime/AggregateExact.lean | 0011 |
Defeasibility — honest heads + defeat directive (#[default] / #[defeats] / #[label]) | §7 | Reasoning/Defeasibility/Transform.leanTypeSystem/Soundness/Defeasibility.lean | 0028 |
Defeasibility — proof tags (+Δ / +∂) | §7 | Reasoning/Defeasibility/Transform.lean | 0028 |
Modal box / diamond (fixed-default) | §7 | Reasoning/Modal.lean | — |
| Recursion-through-negation (well-founded semantics) | §7 | Reasoning/Datalog/WellFounded.lean | — |
Removed defeasible strength triple (#[strict] / #[defeasible] / #[defeater] / #[priority]) | §7 | — | 0028 |
| Rule-atom forms: predicate, comparison, NAF, type-test, field projection | §7 | Reasoning/Rule.lean | — |
Sinks (pub sink, emit) | §7 | — | 0015 |
Stable models (#[brave]) | §7 | Reasoning/Stability.lean | 0003 |
Temporal rule atoms (since, until, ever, …) / FOL quantifiers | §7 | Reasoning/Temporal.leanDecidability/Temporal.lean | — |
Universal quantifier forall v: T where Body, Head | §7 | Reasoning/Rule.lean | — |
#[comptime] build-time lifting | §7 | Substrate/Comptime.lean | 0002 |
check (consistency observers) | §7 | Reasoning/Checks.leanReasoning/StaticDischarge.lean | 0025 |
derive (recursive head-population) | §7 | Reasoning/EvalProgram.leanReasoning/Stratification.lean | — |
fn (pure compute) | §7 | CoreIR/Lowering.lean | — |
mutate — Datalog-style body | §7 | Runtime/MutationSemantics.lean | — |
mutate — imperative body | §7 | Runtime/MutationSemantics.lean | 0015 |
query (read-only) | §7 | Runtime/EngineModuleStore.lean | — |
| MLT level theory as an unprivileged package | §8 | — | 0043 |
Reflective rule-body intrinsics (iof / meta / specializes / extent) | §8 | MetaCalculus/Reflect.lean | 0023 |
TypeRef reflective type-as-value | §8 | Storage/CatalogProjections.lean | 0023 |
| Decidability tier ladder (classifier + per-tier admittance) | §9 | Decidability/Tier.leanDecidability/Classifier.leanDecidability/Admittance.lean | — |
| Decidability — Expressive tier | §9 | Decidability/Expressive.lean | 0003 |
Decidability — Fol tier (unsafe logic { }) | §9 | Decidability/FOL.lean | — |
| Decidability — Metaorder tier | §9 | Decidability/MetaOrder.lean | — |
| Decidability — Modal tier | §9 | Decidability/Modal.lean | — |
| Decidability — Structural / Closure / Recursive tiers | §9 | Decidability/Structural.leanDecidability/Closure.leanDecidability/Recursive.lean | — |
| Polynomial bound on type-graph (D1) evaluation | §9 | Decidability/Complexity/Bounds.leanDecidability/Domain1/Eval.lean | — |
Bridge rules pub bridge | §11 | Standpoint/Federation.lean | — |
| Federation policies (paraconsistent / strict / weighted) | §11 | Standpoint/Consistency.lean | — |
| Sheaf semantics (categorical) | §11 | Locality/SheafEquivalence.lean | 0008 |
Standpoint declarations, lattice <:, standpoint-block scoping | §11 | Standpoint/Visibility.lean | — |
| Standpoints and four-valued federation (Truth4) | §11 | Standpoint/Federation.leanFoundation/Truth4.lean | — |
Truth4 (is / not / can / both) | §11 | Foundation/Truth4.lean | — |
Macros — declarative pub macro, procedural #[procmacro] | §12 | Substrate/Macro.lean | 0037 |
| Traits (behavioral contracts + dispatch) | §12 | TypeSystem/Conformance.leanSubstrate/Trait.lean | 0026 |
ox build → .oxbin (preamble, four version axes, validation, content hash) | §16 | BuildArtifact/Oxbin.leanBuildArtifact/Validation.lean | — |
Bitemporal stamps + time-travel (query … as_of) | §17 | Runtime/AsOf.lean | — |
Forks, hot reload, retention / forget capability | §17 | Runtime/Capability.lean | — |
In-language tests (the test declaration) | §17 | Storage/AxiomKind.lean | 0048 |
Runtime serving ox runtime serve (/v1) | §17 | Conformance/Envelope.lean | 0014 |
| Two-layer incremental cache (compile + runtime) | §17 | Runtime/SalsaContract.lean | — |
Module / Store, execute_mutation, query_* | §17 | Runtime/EngineModuleStore.lean | — |
Glossary
This glossary is not yet written. The defining occurrences of Argon’s core terms live in their home chapters — the meta-calculus vocabulary in the meta-calculus atom, the construct vocabulary in the constructs atom, and the reasoning and world-assumption vocabulary in Part III. A consolidated glossary collecting those definitions is a deferred follow-up.
Walking example
//! Mini lease vocabulary. Exercises all five atoms.
use std::math::{Nat, Money, Date, DateTime};
use std::diag::{Diagnostic, Severity};
use std::format::format;
use std::time::{today, now};
/// A natural person.
pub type Person { name: String, age: Nat }
/// A rentable real-estate unit.
pub type Property { address: String, sqft: Nat }
/// Lease — relation with intrinsic property body.
pub rel Lease(tenant: Person, property: Property) [0..*] [0..*] {
monthly_rent: Money,
start: Date,
end: Date,
status: LeaseStatus,
}
pub enum LeaseStatus { Pending, Active, Expired, Terminated }
impl Person {
/// All active leases held by this person.
pub query active_leases(self) -> [Lease] {
select l from l: Lease, Lease(self, _) where is_active(l)
}
}
impl Lease {
pub fn daily_rent(self) -> Money = self.monthly_rent / 30;
}
pub derive is_active(l: Lease) :- l.start <= today(), l.end > today();
pub mutate sign_lease(t: Person, p: Property, rent: Money, term_days: Nat) -> Lease {
require { rent > 0, term_days > 0 }
let l = insert Lease(t, p) {
monthly_rent: rent,
start: today(),
end: today() + term_days.days,
status: Pending,
};
emit AuditLog { LeaseEvent::Signed { lease: l, at: now() } };
return l;
}
pub check no_overlapping_leases(t: Person, p: Property) :-
count { from l: Lease where l.tenant == t, l.property == p, is_active(l) } > 1
=> Diagnostic {
severity: Severity::Error,
code: "Lease::E001",
message: format!("Tenant {} has overlapping active leases on {}", t.name, p.address),
};
pub trait Renewable { fn renew(self, term_days: Nat) -> Self; }
impl Renewable for Lease {
fn renew(self, term_days: Nat) -> Lease = Lease {
..self,
start: self.end,
end: self.end + term_days.days,
status: Pending,
};
}
#[derive(Json, Hashable)]
pub struct Receipt { lease: Lease, paid: Money, at: Date }
#[consistency(strict)]
pub standpoint CaliforniaTenancyLaw <: BaseTenancyLaw {
pub check ab1482_rent_cap(l: Lease) :-
l.monthly_rent > l.property.market_rate * 1.10
=> Diagnostic {
severity: Severity::Error,
code: "CA::AB1482",
message: "Lease exceeds AB-1482 rent cap",
};
}
pub sink AuditLog: LeaseEvent;
pub enum LeaseEvent {
Signed { lease: Lease, at: DateTime },
Renewed { lease: Lease, at: DateTime },
Terminated { lease: Lease, at: DateTime },
}
test "active leases are returned for tenant" {
let alice = insert Person { name: "Alice", age: 30 };
let unit = insert Property { address: "1 Main", sqft: 700 };
let l = sign_lease(alice, unit, 2500, 365);
assert alice.active_leases() == [l];
}