LANGUAGE REFERENCE · v1.0.4

Every construct, and the Hoon it becomes

yamoon is sugar over Hoon: a readable, YAML-like file compiles to idiomatic Hoon. This page covers each feature — the syntax, the Hoon rune it maps to, and a short why. yamoon ships generics, native Hoon bindings, and a state-aware testing system.

01

Your first file

Every file opens with module:. Below it come named blocks, grouped by indentation like YAML. No runes, no braces. You include only the blocks you use.

Loading editor…

Why: the layout mirrors the structure of the code, so a file reads straight down. module: is the only required block.

02

Functions

A function names three things: input (what it accepts), output (what it returns), and return (the expression it evaluates). The input becomes the Hoon subject; output is the result type; return is the body.

Loading editor…

A function compiles to a Hoon gate (|=). Because input and output are written out, the compiler checks the body returns the declared type. Use cast(Type, Expr) to force a mold (Hoon ^-).

03

Operators, literals and interpolation

Inside return you write ordinary expressions. Operators read the way you expect, and text supports interpolation with {expr}.

form
meaning
+ - *
add, subtract, multiply
== !=
equal, not equal
> < >= <=
comparison
~
null / empty
'"hi, {name}"'
interpolated text
Loading editor…

Interpolated strings are wrapped in single quotes around a double-quoted body; {x + y} can hold any expression. Operators map to Hoon arms (add, mul, gth, …) so you never type a rune for arithmetic.

04

Control flow and the runes

yamoon names a high-level construct for every core Hoon rune. Branching reads as keywords:

yamoon
hoon rune
if: c then: a else: b
?: (branch on c)
if_not: c then: a else: b
?. (inverted branch)
assert: c in: e
?> (require c is true)
unless: c in: e
?< (require c is false)
let: { v: x } in: e
=+ (new variable)
set: { v: x } in: e
=. (mutate variable)
match: e cases: {..} default: d
?+ (switch on a union)
cast(T, e)
^- (force a mold)
nock(f)
.~ (raw VM access)
scry(mark, path)
.^ (kernel scry)
Loading editor…

Nesting if/then/else gives a chain of ?: runes — the same Hoon you would write by hand, without the symbols.

05

Bindings: let and set

let introduces a new named value for the rest of an expression (Hoon =+). set replaces a value already in scope (Hoon =.), which is how you update agent state.

Loading editor…

let: { doubled: x + x } compiles to =+(doubled (add x x) …), then the in: expression runs with doubled in scope.

06

Pattern matching and unions

A union type lists named variants. match inspects a value, picks the matching case, and falls back to default. It compiles to Hoon's ?+ switch, with variant fields extracted for you.

Loading editor…

Each case corresponds to a variant. Unions can be recursive (a variant field whose type is the union itself), which is how you model trees and lists of your own.

07

Loops and tail recursion

There is no for loop. Repetition is tail recursion, written with loop:. You declare the loop's starting args, then call recurse(...) with the next values. Returning a plain value ends the loop.

Loading editor…

loop: compiles to a Hoon trap (|-) and recurse(...) to the buc rune ($), restarting with new args. Because the recursive call is in tail position it runs in constant stack.

08

Types and data

Built-in types are written as plain words; collections and options use angle brackets and ?. Group fields with a record, list alternatives with a union.

yamoon type
hoon
number
@ud
text
cord / tape
bool
?
list<T>
(list T)
pair<A, B>
[A B]
map<K, V>
(map K V)
set<T>
(set T)
T?
(unit T)
Loading editor…

A record compiles to a Hoon structure. constants: holds top-level values. A T? value is built with unit(x) or the empty ~ (Hoon (unit T)).

09

Generics (type_args)

New in 1.0.3. Declare type parameters in type_args, then use them in input and output. The compiler infers them at each call site and enforces consistency — Rust-like strong generics without losing type safety.

Loading editor…

identity(42) binds T = number; identity("hi") binds T = text. If two arguments bound to the same T disagree (e.g. pair<number, text> for pair<T, T>), the compiler raises a Generic Conflict.

10

Lists, maps and sets

List, map, and set operations are functions, not runes. They compile to the matching Hoon arms:

yamoon
hoon
first(l) rest(l)
i.l t.l
prepend(x, l) append(l, x)
[x l] (snoc l x)
map(l, f) filter(l, p)
(turn l f) (skim l p)
fold(l, i, f) length(l)
(roll l f) (lent l)
get(m, k) put(m, k, v)
(~(get by m) k) (~(put by m) k v)
has(c, k)
(~(has by m) k) / (~(has in s) k)
Loading editor…

map<K,V> and set<T> compile to Hoon's by and in engines. A map lookup returns V? (a unit), since the key may be absent.

11

Native Hoon bindings and raw Hoon

New in 1.0.3. Pull in existing Hoon libraries with imports: (the /+ and /- runes), then teach yamoon their signatures with a native: block so calls are still type-checked. When you need Hoon directly, drop to a raw escape.

Loading editor…

For full control, a return (or constant) can be raw Hoon via hoon:, and nock(formula) reaches the bare VM. Object literals like "ja({ a: a })" are rewritten into Hoon's treap structures.

Loading editor…

imports map to /+ and /- runes verbatim. The hoon: escape is the precise tool for cases the high-level forms do not cover (e.g. complex state migrations).

12

Macros

A macro is a shorthand: name it, list its args, and give the shape it expands into. Expansion is pure syntactic substitution that runs before type-checking and compilation, so there is no runtime cost and the output stays explicit.

Loading editor…

A macro can expand into any yamoon shape — a value, a record, or control flow like the assert above — so one call can generate a whole block of Hoon.

13

Subject navigation (wings)

Urbit programs navigate a subject tree. yamoon exposes that directly:

yamoon
meaning
user.name
reach a field of a record
..name
reach an arm/variable in the parent core
^var
skip a local and use the same name from an outer scope

These matter inside loops and nested let/match blocks, where the same name can exist at more than one level.

14

Testing

New in 1.0.3: a state-aware, isolated testing system. Test code is never bundled into production output — yamoon compile emits only the agent/library; yamoon test emits a separate Urbit +test generator.

Unit tests pair inputs with expected outputs for pure functions. Add fuzz: true to property-test with generated inputs.

Loading editor…

Scenario tests drive a Gall agent through a journey of pokes and waits; yamoon threads the state for you and you assert on state and scries:

Loading editor…

For a real check, yamoon sync copies the generated Hoon into a pier and you run -test in the Dojo; bun run test:docker boots a fake ~zod and runs the whole suite through the actual Nock VM.

15

Gall agents

Set options.target to gall to build an agent instead of a library. That unlocks state (data kept between events), pokes (messages in), scries (read-only queries), and watches (subscriptions).

Loading editor…

options.target: gall compiles to an agent door (|_). pure(state) is the no-effect return; to emit cards return [ cards state ]. The version on state lets the compiler generate safe migrations.

16

Reading the generated Hoon

Nothing is hidden. Run yamoon --serve and it recompiles as you type, showing the Hoon it produces: functions as gates, types as structures, macros as their expansion, loops as traps.

Or paste any snippet into the in-browser playground — it runs this exact compiler and prints the Hoon.

Open the playground

17

Limitations

yamoon is production-ready for application development, but there are real boundaries to know about (v1.0.x):

1. Targets: it generates library and gall (agent) code only. It cannot author custom system vanes or mark definition files.

2. Complex migrations: on_load threads the old state vase automatically, but multi-version leapfrogging (v0 → v1 → v2) may need the raw hoon: escape for precise mold mapping.

3. Frontend: yamoon handles the backend Urbit logic only — it does not generate UI code, though it pairs cleanly with @urbit/http-api.

Type mismatches surface as compiler errors with line/column and a path (e.g. In functions.name.return); reach for cast(Type, Expr), the any type, or the raw escape when the high-level forms fall short.

18

Where to go next

Read and edit working files to get fluent. Browse the examples, or compile your own in the playground.