Nockasm#
nockasm is a thin macro layer over canonical Nock, designed to make hand-written Nock more legible. It is not a separate language or a compiler; every nockasm expression expands to a plain Nock noun that you can paste directly into any Nock evaluator. The primary audience is people learning to write Nock by hand or verifying that a piece of Nock does what they think it does.
As of 1.2.0 the library is implemented twice — in Python and in Hoon — and the two expand every program to bit-identical nouns, so the same .nasm source means the same thing in a notebook, in an Urbit dojo, and inside a NockApp.
This notebook uses the Nockasm Jupyter kernel, which assembles each cell to canonical Nock 4K and evaluates the result via pinochle. Output shows the expanded formula on a ;-prefixed comment line, followed by the computed value.
Note
Install the kernel once before opening this notebook:
pip install "nockasm[kernel]"
nockasm-kernel-install
Then select Nockasm from the kernel menu.
Named opcodes#
Raw Nock opcodes are bare numbers. nockasm replaces them with named forms:
|
Canonical Nock |
Meaning |
|---|---|---|
|
|
address slot N in subject |
|
|
produce literal X |
|
|
evaluate F against G |
|
|
test cell/atom |
|
|
increment atom |
|
|
test equality |
|
|
conditional branch |
|
|
compose formulas |
|
|
push F onto subject, eval G |
|
|
call arm at slot N |
|
|
edit slot N of F with V |
|
|
static hint |
|
|
dynamic hint with clue |
Axis aliases name the standard Hoon core slots: (%self) → [0 1], (%battery) → [0 2], (%payload) → [0 3], (%sample) → [0 6], (%context) → [0 7].
; Increment the subject — default subject is atom 0, so result is 1
(%inc (%self))
; [4 0 1]
1
; Test equality of slots 2 and 3 — subject [0 0], result 0 (Nock true)
#subject [0 0]
(%eq (%slot 2) (%slot 3))
; [5 [0 2] 0 3]
0
; Conditional: if slot 2 == 0 return 1, else return slot 2
; subject [0 99]: slot 2 is 0, true branch fires, returns 1
#subject [0 99]
(%if (%eq (%slot 2) (%const 0)) (%const 1) (%slot 2))
; [6 [5 [0 2] 1 0] [1 1] 0 2]
1
Axis schemas#
:subject {.a .b .c} binds names to the standard right-leaning binary tree addresses (axis 2, 6, 7 for a three-element subject, following Hoon convention). After the declaration, .a, .b, .c expand to [0 2], [0 6], [0 7] respectively.
The tree layout for a two-element subject [x y]:
1
/ \
2 3
(x) (y)
For three elements [x y z] (right-leaning: [x [y z]]):
1
/ \
2 3
(x) / \
6 7
(y) (z)
; Two-element subject: .x at axis 2, .y at axis 3
; [5 5] -> .x == .y, result 0 (Nock true)
#subject [5 5]
:subject {.x .y}
(%eq .x .y)
; [5 [0 2] 0 3]
0
; Three-element subject: .target at axis 6 — increment it
#subject [10 41 99]
:subject {.before .target .after}
(%inc .target)
; [4 0 6]
42
Structural macros#
Beyond named opcodes and axis names, nockasm provides exactly two structural macros, each a fixed lowering with no hidden semantics, plus the :subject declaration they build on:
Form |
Expands to |
Meaning |
|---|---|---|
|
(compile-time only) |
name the subject’s axes; right-leaning by Hoon convention |
|
|
local binding: push |
|
|
evaluate the scrutinee once, dispatch on noun-literal patterns; the |
Everything else on the page is pure notation: named opcodes are single cells, .name is [0 axis], and raw cells [a b c] pass through structurally (the escape hatch into hand-written Nock). The two macros are the only forms that rearrange the subject — and both do it the same way, by pushing with opcode 8 and re-rooting the names in scope.
#let .name = VALUE in BODY#
Pushes VALUE onto the subject via opcode 8, binding .name to the new head (axis 2). Existing schema names shift rightward through peg(3, axis) so they remain valid in BODY.
This is the nockasm equivalent of a local let binding: it evaluates VALUE against the current subject and pushes the result as a new head, making it available under .name while the body runs.
; Bind .next to the incremented .target, then return all three values.
; subject [10 41 99] -> [10 42 99]
#subject [10 41 99]
:subject {.before .target .after}
#let .next = (%inc .target) in
[.before .next .after]
; [8 [4 0 6] [0 6] [0 2] 0 15]
[10 42 99]
#match EXPR { PAT => BODY ... _ => DEFAULT }#
Evaluates EXPR once via opcode 8, then dispatches on its value using nested opcode-6 branches. The _ => default branch is required. Patterns are noun literals compared against the scrutinee at runtime.
Each pattern arm compiles to a nested [6 [5 [1 PAT] 0 2] BODY ...] chain, so a three-arm match with a default produces two nested if branches sharing a single evaluation of the scrutinee.
The same formula handles all three subjects below — the expansion (; line) is identical each time.
; tag=1: increment .data -> 42
#subject [1 41]
:subject {.tag .data}
#match .tag {
1 => (%inc .data)
2 => .data
_ => 0
}
; [8 [0 2] 6 [5 [1 1] 0 2] [4 0 7] 6 [5 [1 2] 0 2] [0 7] 1 0]
42
; tag=2: return .data -> 41
#subject [2 41]
:subject {.tag .data}
#match .tag {
1 => (%inc .data)
2 => .data
_ => 0
}
; [8 [0 2] 6 [5 [1 1] 0 2] [4 0 7] 6 [5 [1 2] 0 2] [0 7] 1 0]
41
; tag=9: default arm -> 0
#subject [9 41]
:subject {.tag .data}
#match .tag {
1 => (%inc .data)
2 => .data
_ => 0
}
; [8 [0 2] 6 [5 [1 1] 0 2] [4 0 7] 6 [5 [1 2] 0 2] [0 7] 1 0]
0
Combining features#
The macros compose freely. Here is a small decrement-style program: given a subject [n acc], match on n and either return acc when n is zero or recurse (expressed as #let + #match) incrementing acc.
; Nested let + match: .a == .b (both 5), so equality is 0 (true),
; the 0-branch fires and increments .a -> 6
#subject [5 5]
:subject {.a .b}
#let .eq = (%eq .a .b) in
#match .eq {
0 => (%inc .a)
_ => .b
}
; [8 [5 [0 2] 0 3] 8 [0 2] 6 [5 [1 0] 0 2] [4 0 14] 0 15]
6
Hints and jets#
Opcode 11 hints code: it is an annotation that never changes the value computed, but may be used by a runtime for side effects or acceleration. nockasm exposes both forms directly:
(%hint T F)→[11 T F]— a static hint: tagT, continuationF.(%hintd T C F)→[11 [T C] F]— a dynamic hint, whose clueCis computed alongside.
The tag is just a noun, so a @tas label is written as a quoted cord. Both forms expand to plain nouns and stay transparent — a runtime that does not recognize the tag computes exactly the same value. Two tags are worth knowing:
; %slog prints a tank, then produces its body. Transparent: the value is the
; body's (1); a jet-aware runtime additionally logs the tank [priority tank].
(%hintd 'slog' [1 1 42] (%inc (%self)))
Exception: fail: cell
Traceback (most recent call last):
File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/nockasm_kernel/kernel.py", line 67, in do_execute
output = self._dispatch(code.strip())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/nockasm_kernel/kernel.py", line 123, in _dispatch
return self._assemble_eval(code)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/nockasm_kernel/kernel.py", line 210, in _assemble_eval
result = _nock_eval(self.subject, formula)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pinochle/nock.py", line 282, in nock
return nock(a, d)
^^^^^^^^^^
File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pinochle/nock.py", line 181, in nock
return lus(result)
^^^^^^^^^^^
File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pinochle/nock.py", line 72, in lus
raise Exception("fail: cell")
Exception: fail: cell
%fast: jetting#
The %fast tag conventionally registers a jet (short for “jet-accelerated code”), a native implementation which a runtime may run in place of a Nock formula. It is written as a dynamic hint wrapping a core:
(%hintd 'fast' 'dec' <the dec core>)
; -> [11 [%fast %dec] <core>] == [11 [1953718630 6514020] <core>]
This transparently produces the core, but as a side effect it warms the core’s battery. Later, when an arm of that core is invoked (opcode 9), a jet-aware runtime matches the battery and runs the native named by the clue instead of walking the formula. pinochle, which this kernel uses, implements this register-then-match dispatch keyed on the battery’s mug with built-in arithmetic jets dec, add, sub, mul, and lte. The native code reads the gate’s sample at axis 6, so a jetted core and its formula agree bit for bit; only the cost differs.
See Hints & Jetting for the runtime side, and Opcode 11: Hint for the opcode itself.
Relation to canonical Nock#
nockasm is a preprocessor, not a language. Every expression it accepts expands to a noun that is bit-identical to what you would write by hand. There is no runtime, no separate VM, and no overhead beyond the expansion step itself. The benchmark programs bundled with the library (dec, add, factorial, fibonacci, ackermann) each expand to nouns bit-identical to the corresponding reference formulas from the Urbit benchmark suite.
That claim is enforced mechanically: the grammar has three independent executors — the Python library, the Hoon library running on Vere, and the Hoon library compiled to Nock running on NockVM (nasmc) — and CI holds all three to byte-identical output on every case in the test corpus.
Note
See The Assembly Language Approach for background on the relationship between Nock and conventional assembly language. nockasm has a similar relationship to canonical Nock as assembly language has to machine code: it is a more legible way to express the same formulas, but it is not a separate language or runtime. But it’s not a machine-style assembly language.
CLI#
Outside of notebooks, nockasm can be driven from the shell:
python -m nockasm program.nasm # canonical flat Nock
python -m nockasm --pretty program.nasm # explicit binary cells
echo "(%inc (%self))" | python -m nockasm
python -m nockasm --from-jam formula.jam # jammed formula -> .nasm source
The CLI reads .nasm source files or stdin and writes the expanded noun to stdout, making it easy to pipe into any Nock evaluator or use in a build pipeline. There is also a standalone compiler binary, nasmc, which does the same work as a NockApp — the expander itself compiled to Nock and run on NockVM — and emits jammed formulas directly:
nasmc program.nasm # -> program.jam (raw formula jamfile)
nasmc --text program.nasm # canonical flat noun to stdout
nasmc --render program.nasm # canonical .nasm formatting
nasmc --lift formula.jam # jamfile back to .nasm source
Round trips: jamfiles back to source#
Nock artifacts in the wild are usually jamfiles — serialized nouns, whether a 70-byte formula or a multi-megabyte kernel. As of 1.2.0 nockasm can read a jammed formula back to legible source (--from-jam above, nasmc --lift, or lift/nasm_from_jam in the API). For example, the #let program from earlier in this notebook expands to [8 [4 0 6] [0 6] [0 2] 0 15]; jam that noun, lift it back, and you get:
(%push (%inc (%slot 6)) [(%slot 6) [(%slot 2) (%slot 15)]])
Note what survived and what did not. The structure is fully recovered as named opcodes — but the #let sugar and the schema names are gone, because those were compile-time constructs that lowered away. And the lift is deliberately zero-heuristic: Nock is homoiconic, so nothing in a noun marks it as code. The lift reads a noun as a formula only where Nock’s positional grammar proves the shape, falls back to plain cells everywhere else, and never guesses intent — an opcode-1 constant renders as data even if it happens to be a battery. What you gain from that honesty is a hard guarantee: the emitted source always re-expands to the exact noun you started from.
A compiler target, and everywhere Nock lives#
Beyond hand-writing, 1.2.0 promotes the parsed AST to a versioned public IR — parse / lower / render in both implementations — governed by a round-trip law: rendering any IR value to text and re-expanding it produces bit-identically the same noun as lowering it directly. A compiler can emit nockasm IR and get legible, diffable .nasm build artifacts whose meaning is guaranteed, which is the intended path for higher-level languages targeting Nock. The design is specced in doc/compiler-target.md.
The same source travels across the ecosystem:
Urbit — the Hoon library lives at
desk/lib/nockasm.hoon((expand:nasm '...')in the dojo), and a%nasmclay mark lets.nasmfiles live in desks like any source file, with the source cord as noun form (exactly parallel to%hoon).NockApps — the library is registered in the typhoon registry, so any Nockup project can add
"sigilante/nockasm" = "latest"and/+ nockasmin its kernel;hooncbuilds also load.nasmfiles directly via/*.The shell —
nasmc, above: Nock assembly compiled to Nock by Nock.