Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Computations and Mutations

derive and query give you reasoning. Most real ontologies need two more things: pure functions over the data (a lease’s days-until-expiry, the total rent owed) and operations that change the world (signing the lease, expiring it). Argon has dedicated item kinds for both.

compute is the function form: input → output, no side effects, fully evaluable as an expression. mutation is the state-changing form: imperative body, audit-trailed events, with bitemporal valid-time and transaction-time semantics modeled at the language surface and at the kernel API level. (ox mutate accepts --principal, --standpoint, --valid-at, and --idempotency-key; the in-process runtime applies them against the current knowledge store, while a fully durable bitemporal log lives in the kernel API tier — see Part 4.)

This chapter teaches both. The lease-tutorial examples below model parties as UFO kind / role concepts — those metatypes are user-declared via the substrate of Chapter 2.1, not built into the language; computations and mutations themselves are vocabulary-neutral.

compute — pure functions

The simplest compute is one expression:

pub compute annual_rent(l: Lease) -> Money =
    l.monthly_rent * 12

Read in English: “the function annual_rent, given a Lease, returns twelve times its monthly rent.”

A few mechanics:

  • compute is a hard-reserved keyword. Like derive and mutation, the name introduces a kind of item, not a type.
  • -> Money is the return type. Argon will not infer it; computes always declare their return type explicitly. This is intentional — the return type is part of the function’s contract.
  • = expr is the body. A single expression. No statements, no early returns, no side effects.

The body uses ordinary arithmetic over the primitive types (Int, Decimal, Money) and the operators in Appendix B. Date arithmetic — durations between dates, .days-style accessors, calendar-aware arithmetic — lives in domain packages today; the language core ships only the primitive types.

Three forms

compute admits three body shapes, each useful in different situations.

Form 1 — single expression

pub compute total_rent(l: Lease) -> Money =
    l.monthly_rent * months_between(l.start_date, l.end_date)

The most common form. Reads like a definition; lifts trivially into rule bodies; participates in the meta-property calculus and the tier classifier.

Form 2 — block with a body expression

When the computation needs intermediate bindings:

pub compute deposit_after_deductions(d: SecurityDeposit) -> Money {
    body {
        let allowed = sum(x.amount for x in d.deductions where x.is_allowed)
        let disallowed = sum(x.amount for x in d.deductions where not x.is_allowed)
        d.held_amount - allowed
    }
}

The body { let ... ; let ... ; <expr> } block carries let-statements (intermediate bindings, never side-effects) followed by a trailing expression that is the computed value. Form 2 looks imperative but evaluates as a pure expression — the engine inlines the lets and reads the trailing expression as the result.

Use Form 2 when an intermediate name makes the computation readable. When you find yourself writing five lets in a row, think about whether the intermediate values deserve to be their own computes — small composable functions read better than big block bodies.

Form 3 — Rust FFI

Some functions cannot be expressed in Argon — cryptographic primitives, regex matching, OS clock access. For those:

pub compute hash_id(input: String) -> String {
    impl rust("crypto_helpers::sha256_hex")
}

The impl rust("path::to::function") clause delegates to a Rust function bundled with your toolchain. Form-3 computes are opaque to the reasoner — the type-checker still validates the signature, but the body is dispatched through the runtime’s FFI surface. Use them sparingly; every Form-3 binding is a place the reasoner cannot see.

Tip: if a Form-3 compute crops up in a hot rule body, look for a way to refactor: either lift the rust call to a precomputed field (an asserted fact rather than a derived one), or rewrite the rule to keep the FFI off the inner loop.

Compositional bodies

Compute bodies admit the full expression vocabulary: arithmetic, if/else, match, aggregates, list literals, function calls. Tier-classification composes by taking the maximum tier across components, so if and match do not introduce new tiers — they propagate the tier of their branches.

Status note: if-condition type rule. The type rule for if-conditions (that they must be Bool-typed) is part of the upcoming Appendix B operator audit. Today’s elaborator accepts non-Bool conditions like if s.rank { … } where s.rank: Nat; tracked at sharpe-dev/orca-mvp#412.

pub compute lease_status(l: Lease) -> String =
    if l.end_date <= today() {
        "expired"
    } else if l.start_date > today() {
        "pending"
    } else {
        "active"
    }
pub compute lease_summary(l: Lease) -> String =
    match l {
        ResidentialLease(r) => "Residential lease, " + r.bedrooms + " bedroom",
        CommercialLease(c) => "Commercial lease, use: " + c.permitted_use,
        _ => "General lease",
    }

Aggregates lift naturally:

pub compute total_collected(l: Lease) -> Money =
    sum(p.amount for p in l.payments where p.cleared)

The for var in path where atom clause filters the iteration. The filter atom is the same predicate vocabulary as rule bodies.

mutation — operations that change the world

Where compute produces a value, mutation changes the model. A mutation can update fields, retract derived facts, emit typed events into the bitemporal log, and return a value.

pub mutation sign_lease(l: Lease, signed_at: Date) -> Lease {
    require {
        l.start_date < l.end_date
    }
    do { }
    emit LeaseSigned(l, signed_at)
    return l
}

Five clauses, all but do optional:

ClauseCardinalityPurpose
require { body }optional, oncePrecondition — body must hold for the mutation to fire.
retract { stmts }optional, onceFacts to remove from the knowledge base.
do { stmts }mandatory, onceField updates and intermediate lets.
emit <expr>repeatsEvents to publish into the axiom-event log.
return <expr>optional, onceReturn value (matches the declared -> T).

The recommended ordering is the table above. The parser is order-permissive — you can write emit before do — but the convention reads better in order.

A worked example using all five clauses. The retract clause removes facts from the knowledge base before the new state asserts; emit publishes the supersession event into the bitemporal log:

pub mutation correct_rent(l: Lease, new_rent: Money) -> Lease {
    require {
        new_rent > 0,
        l.monthly_rent != new_rent
    }
    retract {
        MonthlyRent(l, l.monthly_rent)
    }
    do {
        l.monthly_rent = new_rent
    }
    emit RentCorrected(l, new_rent, today())
    return l
}

Variants: _catalog/mutation-retract-clause/{minimal,composition,bitemporal,idiom}/. The catalog’s idiom/ variant pairs retract with a follow-up emit of a supersession axiom — the canonical pattern for keeping the bitemporal log audit-honest when a value is corrected.

Migration note: the team is exploring removing the do keyword: a mutation body would just be raw statements, with lets and field updates appearing directly. The emit clauses stay where they are. Today’s do { } block continues to compile; the migration will land additively. See Appendix D.

Field updates

The do { } block is where state actually changes:

pub mutation pay_rent(l: Lease, amount: Money) {
    require {
        amount > 0,
        amount <= l.monthly_rent
    }
    do {
        let new_balance = l.balance + amount
        l.balance = new_balance
    }
    emit PaymentReceived(l, amount, today())
}

Two statement forms inside do { }:

  • let name [: Type] = expr — bind an intermediate value (not a state change).
  • <field-path> = expr — set a field value (state change).

There is no compound assignment (+= or similar) yet. The Phase-B agenda has compound-assignment as an open ergonomics item; for now, write the equivalent: l.balance = l.balance + amount.

Events

emit Expr publishes the value of Expr into the bitemporal axiom-event log. The engine wraps the emission with three pieces of metadata: a valid time (when the event becomes true in the world), a transaction time (when the system learned of it), and a principal (which agent caused the emission, supplied via the --principal flag on ox mutate).

Events are first-class typed values. A user-defined event:

pub kind PaymentReceived {
    lease: Lease,
    amount: Money,
    received_at: Date,
}

Then emit PaymentReceived(l, amount, today()) constructs an instance and publishes it. The runtime’s bitemporal storage treats it as an append-only fact.

Migration note: events are moving toward being a first-class language primitive — a dedicated event item kind that ties consequences in rules and mutations through the same declared event types. Today, declare the event as a kind with the relevant fields; the migration will introduce event declarations as a richer alternative. See Appendix D.

Bitemporal axes — what the runtime gives you

Every event in the log carries two times. Concretely:

  • Valid time — the time at which the event is true in the world. By default, today() at the moment of emission, but you can override with ox mutate --valid-at #2026-01-01#.
  • Transaction time — the time at which the system learned of the event. Always the wall clock at emission; never overridable.

Queries can dial in either axis:

$ ox query active_leases --as-of-valid #2026-04-01#
   What was active on April 1?

$ ox query active_leases --as-of-tx #2026-04-15#
   What did we know was active, as of the database state on April 15?

--as-of-valid is “go back in time in the world.” --as-of-tx is “go back in time in the database.” They are independent: you can ask “as of April 15 in the database, what was active on April 1 in the world?” by combining them.

This is built in. You do not write any of it.

Forks

Sometimes you want to reason in a what-if world without mutating the canonical one. Argon’s runtime gives you forks:

$ ox mutate sign_lease --fork experimental --principal user-1 --arg l=lease-001
   Mutation applied to fork=experimental.

$ ox query active_leases --fork experimental
   ...sees the signed lease in the experimental fork only.

A fork is a structural-shared branch of the world. Mutations against a fork do not flow back to MAIN unless you explicitly promote them. We will see more of this in Chapter 5.4; here the point is just that mutations are fork-aware.

Reading the consequences

ox mutate --explain shows the mutation’s effects:

$ ox mutate sign_lease --principal user-1 --arg l=lease-001 --arg signed_at=#2026-01-15# --explain
   Preconditions:
     ✓ l.start_date < l.end_date

   Field updates:
     (none)

   Events emitted:
     LeaseSigned(lease-001, #2026-01-15#)
       valid_time = #2026-01-15#
       tx_time    = #2026-04-24T10:32:11Z#
       principal  = user-1

   Returned: lease-001

--dry-run runs the mutation without committing it — useful for testing whether preconditions hold and what events would land.

Edge cases worth knowing

  • compute Form-2 cannot emit events. That is the line between compute and mutation: computes are referentially transparent.
  • require is a precondition, not a guard. If require’s body does not hold, the mutation does not fire and an error is reported. It is not “skip silently.”
  • mutation items must be pub to be runtime-dispatchable. The CLI dispatches by qualified name; non-pub mutations are file-scoped and unreachable from outside.
  • --principal is mandatory on ox mutate. The runtime refuses to apply a mutation without an attribution. Set it; pass it; track it. The audit trail is the point.
  • Event types should be declared as concepts. Inline emit of an ad-hoc tuple loses you typed-querying later. Declare a kind for every event you emit; it pays back the keystrokes immediately.

Putting it in the running example

Add src/computes.ar:

use lease::*

pub compute annual_rent(l: Lease) -> Money =
    l.monthly_rent * 12

Add src/mutations.ar:

use lease::*

pub kind LeaseSigned {
    lease: Lease,
    signed_at: Date,
}

pub kind LeaseExpired {
    lease: Lease,
    effective_at: Date,
}

pub mutation sign_lease(l: Lease, signed_at: Date) -> Lease {
    require {
        l.start_date < l.end_date
    }
    do { }
    emit LeaseSigned(l, signed_at)
    return l
}

pub mutation expire_lease(l: Lease, effective_at: Date) -> Lease {
    do { }
    emit LeaseExpired(l, effective_at)
    return l
}

Update prelude.ar:

pub use metatypes::*
pub use party::*
pub use lease::*
pub use rules::*
pub use queries::*
pub use computes::*
pub use mutations::*

Then:

$ ox check
   Checking lease-tutorial v0.1.0
    Finished in 0.12s

The model now derives, queries, computes, and mutates. We have all three computational modes wired up.

Summary

compute is for pure functions; mutation is for state changes. compute has three body forms — single expression, block, and Rust FFI — picked by what the function does. mutation has five clauses — require, retract, do, emit, return — and operates against a bitemporal, fork-aware, audit-trailed runtime. The lease tutorial now signs and expires leases. Next we tighten the type system around the model.