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.
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.
Why: the layout mirrors the structure of the code, so a file reads straight down. module: is the only required block.
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.
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 ^-).
Operators, literals and interpolation
Inside return you write ordinary expressions. Operators read the way you expect, and text supports interpolation with {expr}.
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.
Control flow and the runes
yamoon names a high-level construct for every core Hoon rune. Branching reads as keywords:
Nesting if/then/else gives a chain of ?: runes — the same Hoon you would write by hand, without the symbols.
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.
let: { doubled: x + x } compiles to =+(doubled (add x x) …), then the in: expression runs with doubled in scope.
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.
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.
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.
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.
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.
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)).
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.
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.
Lists, maps and sets
List, map, and set operations are functions, not runes. They compile to the matching Hoon arms:
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.
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.
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.
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).
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.
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.
Subject navigation (wings)
Urbit programs navigate a subject tree. yamoon exposes that directly:
These matter inside loops and nested let/match blocks, where the same name can exist at more than one level.
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.
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:
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.
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).
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.
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.
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.
Where to go next
Read and edit working files to get fluent. Browse the examples, or compile your own in the playground.