Hello, Argon
Argon programs start the same way Rust programs do: with the smallest piece of code that compiles. We will write that piece first and then walk through what every line means.
The smallest program
Argon does not have a single-file mode. The smallest meaningful unit is a project — a directory with an ox.toml manifest and a src/ tree of .ar modules. Set one up:
$ mkdir -p hello-argon/src
$ cd hello-argon
Write a manifest at ox.toml:
[project]
name = "hello-argon"
version = "0.1.0"
edition = "2025"
entry = "root.ar"
[schema]
root = "src"
[package]
name = "hello-argon"
version = "0.1.0"
edition = "2025"
[dependencies]
Chapter 1.3 explains the manifest’s two-section structure ([project] for the perspectival-composition layer; [package] for the registry layer); for now treat it as boilerplate.
The program we are about to write declares its metatype machinery inline so you can see Argon’s substrate forms (pub metaxis, pub metatype) at work. In a production project you would normally write use ufo::prelude::* and pull in a vocabulary package’s catalog (kind, subkind, role, …) instead — kind is not a built-in keyword, but a metatype declared by the UFO package on top of the same substrate. Either way, the substrate is what the language actually ships; Chapter 2.1 covers it in full.
Then write src/root.ar:
use std::math::String
pub metaxis rigidity for type {
rigid,
anti_rigid
}
pub metaxis sortality for type {
sortal,
non_sortal
}
pub metatype kind = { rigid, sortal }
pub kind Person {
name: String,
}
pub derive HasName(p: Person) :-
p: Person,
p.name != ""
pub query AllNamedPersons() -> [Person] :-
?p: Person,
HasName(?p)
=> ?p
The use std::math::String line at the top is required: Argon ships no implicit prelude, so any primitive a module references — String, Nat, Int, Real, Bool, Date, DateTime, Duration, Decimal, Money — needs an explicit import path. Bare primitive names without a use produce OE0101 UnresolvedIdentifier with a hint at the right import.
Run the type-checker:
$ ox check
ox hello-argon v0.1.0 (./ox.toml)
ox 1 modules resolved from entry point
ox 1 standpoints: default
check passed
Clean. We have an Argon program.
Walking through the program
Eight things happen in twenty-odd lines. Take them in order.
pub metaxis rigidity { rigid, anti_rigid }
Argon ships no foundational metatypes in its language core. The keywords kind, subkind, role, phase, relator — none of them exist as built-in concept-keywords. They are defined by user packages.
A metaxis declares a dimension along which metatypes vary. rigidity is one such dimension; its values are the IDENT atoms rigid and anti_rigid. The mechanism is open-ended — domain modellers in finance, biology, or law can declare their own axes (sortality, criticality, regulability, …) without changing the language.
rigidity and sortality together are enough to define the metatypes Hello, Argon needs. The axes capture (loosely) Guizzardi’s UFO classification: rigidity distinguishes “instances belong to the type permanently” from “instances belong contingently”; sortality distinguishes “the type provides identity criteria” from “the type classifies but does not individuate.” Chapter 5.1 covers the calculus formally.
pub metatype kind = { rigid, sortal }
A metatype declaration introduces a concept-keyword and pins its position along the axes. kind is now a name the rest of this file can use to declare concepts: a kind is rigid (instances belong to it for as long as they exist) and sortal (it supplies its own identity criteria).
This is the line that makes kind exist. If you delete it, the next line stops parsing.
Note: in production code you would normally
use ufo::prelude::*(or similar) and pull in a curated metatype catalog instead of declaring axes and metatypes inline. This chapter shows the mechanism in full so you can see whatusewould otherwise hide.
pub kind Person { name: String, }
A concept declaration. Person is the name; kind is the metatype (the IDENT before the name); the body lists fields with their types. The trailing comma is allowed — keep it on, since the next chapter’s version of Person will grow more fields.
String is one of Argon’s primitive types. The full set is Nat, Int, Real, Decimal, String, Bool, Date, DateTime, Duration, and Money. Each lives in std::math and reaches a module via explicit use.
pub derive HasName(p: Person) :- p: Person, p.name != ""
A rule. HasName is the head; (p: Person) is its argument list with a single typed binding; :- separates the head from the body; p: Person, p.name != "" is the body.
A few mechanics worth pinning down:
- Head parameters bind bare names.
pin the head is the binding; the body’s atoms reference it without a prefix. The?prefix is reserved for body-fresh variables — those introduced by an aggregate’s iteration clause or a query’s body when no head parameter is in scope. Head parameters never carry?. - Comma is the conjunction separator for rule bodies. There is no
&&and noandkeyword inside a body — just commas. - Type tests bind.
p: Personis the body’s first atom; it both testsp’s type and re-assertsp’s binding for the rest of the body. Subsequent atoms seepnarrowed toPerson.
Reading the rule in English: “for any Person p, HasName(p) holds when p.name is non-empty.”
pub query AllNamedPersons() -> [Person] :- ... => ?p
A query is a first-class item: declared, type-checked, callable from the runtime by name. AllNamedPersons takes no arguments and returns a list of Person. The body looks like a rule body. The optional => head_expr after the body says what the result row is.
Because the query head takes no parameters, the body introduces a fresh variable ?p. That is the one place the ? prefix appears: query bodies (and aggregate sub-comprehensions) introduce body-fresh variables explicitly.
Read in English: “return every Person ?p for whom HasName(?p) holds.”
The [Person] syntax is a collection type: a list of Person. We will see cardinality-constrained variants like [Tenant; >= 1] (at least one tenant) in Chapter 2.3.
Running the query
Type-check is ox check. To actually evaluate the query, we need a fact in the world. The runtime command is ox query:
$ ox query AllNamedPersons
No facts asserted; result set is empty.
That is correct. We have declared a kind, a derive rule, and a query — but we have not yet asserted any Person facts. Once we have a package and a test scaffold (Chapter 3.3), asserting facts and seeing query results will be straightforward.
What you noticed
Three things, probably.
No semicolons, but commas matter. Field lists are comma-separated; rule bodies are comma-separated; argument lists are comma-separated. Trailing commas are allowed. Statements inside imperative blocks (we have none yet — they appear in mutation bodies) do not need separators.
Whitespace is friendly, not significant. You can split a long rule body across lines or keep it on one. The compiler does not care.
Every item is opt-in to export. The default visibility is module-internal — file-scoped. Adding pub makes an item reachable from another module. There is no third tier (no per-package or per-workspace visibility); just the two.
Edge cases worth knowing
- Rule bodies want a typed binding for every variable.
p.name != ""readsp’snamefield, but the body needs at least one atom that pinsp’s type — typically a leading type test likep: Person. The compiler rejects rules whose bodies reference identifiers no atom binds. derivewith no asserted instances will not fire.pub derive Always(p: Person) :- p: Persononly derivesAlways(p)for assertedPersoninstances; declaring a kind does not by itself create instances.- Type errors land at
ox check, not at runtime. Misspellingnameasnmein the body ofHasNameraises an error at the field-access; the query never reaches the runtime.
Putting it in the running example
The next chapter promotes this code into the lease tutorial — the running example the rest of the book extends. The metatypes we declared inline here will move into a small metatypes.ar module you write once and never re-think.