Nock Assembly
Nock Assembly is a thin macro over Nock ISA designed to make the language more legible for pedagogical purposes.
Status ~2026.7.31: Nockasm is considered frozen unless Nock ISA decrements to 3K or a bug is found.
Design
| Named opcodes | (%inc .x) instead of [4 0 2]. Pure lexical. |
| Axis schemas | :subject {.a .b .c} resolves .a .b .c to axes 2, 6, 7. Right-leaning by Hoon convention. |
#let .name = E in B |
Opcode-8 push. Tracks subject shift via +peg(3, n) so old names still resolve in body. |
#match E { ... } |
Scrutinee lifted once via opcode 8. Nested opcode-6 dispatch on literal patterns. Required _ => default. |
; comments |
And whitespace. |
Install / use
pip install nockasm
from nockasm import expand
print(expand("(%inc (%self))"))
# [4 0 1]
print(expand("""
: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]
End-to-end with pinochle:
from pinochle import nock, parse_noun
from nockasm import expand
src = """
:subject {.before .target .after}
#let .next = (%inc .target) in
[.before .next .after]
"""
formula = parse_noun(expand(src))
result = nock(parse_noun("[10 41 99]"), formula)
# result == [10 42 99]
At the CLI:
python -m nockasm program.nasm # canonical flat
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
--from-jam cues a jamfile and lifts the noun back to canonical
Nockasm: named ops wherever Nock's positional grammar proves the
shape, structural raw cells everywhere else, no guessed intent.
Sound by construction: the emitted source re-expands to the exact
jammed noun.
Integration with the Nock kernel
Pinochle ships nock-kernel for Jupyter (Nock 4K kernel). It accepts
canonical Nock in :formula cells. Workflow today:
- Write
.nasmin a regular Python cell (or text editor). - Run
expand(src)in a Python notebook to get canonical Nock. - Paste the result into a
:formulacell in a Nock notebook.
A :asm cell magic for the Nock kernel that does this in one step is the
obvious next step. Roughly:
# in pinochle/packages/nock_kernel/kernel.py
if cell.startswith(':asm'):
from nockasm import expand
formula_src = expand(cell[len(':asm'):])
# then dispatch as if user had typed ':formula <formula_src>'
Hoon library
desk/lib/nockasm.hoon is a port of the expander to Hoon: .nasm source
in as a cord, Nock formula out as a noun. The parser is written in the
++rule combinator idiom; errors crash with tagged traces
(%unbound-axis, %unknown-opcode, %let-shadows, …). The shared
types — the +nasm-of vocabulary builder, $nasm, $sema,
+nasm-version — live in desk/sur/nockasm.hoon, which the library
imports; downstream toolchains that only construct or inspect IR
vendor the sur without the implementation.
> =nasm -build-file %/lib/nockasm/hoon
> (expand:nasm '(%inc (%self))')
[4 0 1]
> .*([10 41 99] (expand:nasm ':subject {.a .b} .b'))
[42 99]
The noun is directly usable with .* — no text round-trip. The Python
suite is the conformance oracle: tests/test_hoon.py expands every unit-test
source and all five benchmarks through both implementations via
urbit eval and compares nouns bit-for-bit (no ship required; set
URBIT_BIN to your vere binary). desk/tests/lib/nockasm.hoon carries
a representative subset for the on-ship test framework:
> -test %/tests/lib/nockasm ~
Both implementations also expose the parsed AST as a versioned compiler
target IR — parse / lower / render, with render byte-identical
across the two and governed by the round-trip law
expand(render(x)) == lower(x) — plus jam / cue / lift /
nasm-from-jam for reading jammed formulas back to source under the
soundness law lower(lift(f)) == f. See doc/compiler-target.md.
Use in a NockApp
The library compiles unmodified under hoonc (vendor
desk/sur/nockasm.hoon into hoon/sur and desk/lib/nockasm.hoon
into hoon/lib; hoonc resolves the /- import from the sur tree)
and is registered in the
typhoon registry, so any
Nockup
project can depend on it by name:
[dependencies]
"sigilante/nockasm" = "latest"
Then /+ nockasm in your kernel and call expand:nockasm (cord in,
formula out), render:nockasm, or nasm-from-jam:nockasm at runtime —
or precompute formulas at build time via /#.
desk/mar/nasm.hoon is a clay mark for .nasm files (noun form: the
source cord, parallel to %hoon), so the same files live in Urbit
desks, hoonc builds (/* f %nasm /path loads octs), and text
tooling alike. See doc/nasm-mark.md.
nasmc
nasmc/ is the compiler as a standalone binary — a NockApp modeled on
hoonc, with the Hoon library compiled into its kernel, so expansion
runs as Nock on nockvm:
cd nasmc && cargo build --release
nasmc program.nasm # -> program.jam (raw formula jam)
nasmc --text program.nasm # canonical flat noun to stdout
nasmc --render program.nasm # canonical formatting to stdout
nasmc --lift formula.jam # jamfile back to .nasm source
tests/test_nasmc.py holds it to the same standard as everything else:
every corpus jam byte-identical to the Python oracle — a third
independent executor (CPython, Hoon-on-vere, Hoon-on-nockvm) of the
same laws. See nasmc/README.md.
Structural macros
#let .name = VALUE in BODY
Pushes VALUE onto the subject via opcode 8 and binds .name to axis 2 in
BODY. Any axes that were already in scope are shifted rightward via
+peg(3, axis), so the old names still resolve in the body.
:subject {.before .target .after}
#let .next = (%inc .target) in
[.before .next .after]
; -> [8 [4 0 6] [0 6] [0 2] 0 15]
; against [10 41 99] -> [10 42 99]
VALUE and BODY are both formula positions (bare atoms lift). Shadowing
an existing schema name is a compile error.
#match EXPR { PAT => BODY ... _ => DEFAULT }
Pattern match on the value of EXPR. The scrutinee is evaluated once via
opcode 8 — i.e. lifted onto the subject — then each PAT is compared
against the lifted value via opcode 5 (eq), with opcode 6 (if) dispatching
to the matching BODY. The _ => default is required.
: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]
; against [1 41] -> 42
; against [2 41] -> 41
; against [9 41] -> 0
EXPR and each BODY are formula positions. PATs are noun literals —
they're compared against the scrutinee's runtime value, not against a
formula. Bare atoms in PAT position are not lifted: writing 1 => ...
matches the atom 1, not the formula [1 1].
In the body of each arm (and the default), the scrutinee is at axis 2, and
the original schema axes are shifted rightward via +peg(3, axis) — same
shift rule as #let. That's why .data resolves to [0 7] (not [0 3])
in the example above.
What lifts and what doesn't
Bare atoms get lifted to [1 atom] in formula positions. Not in noun-literal
positions (%const arg, hint tag) or axis positions (%slot arg, %call
arity arg, etc.). The per-opcode kinds:
| Opcode | Kinds | Notes |
|---|---|---|
%slot N |
a | axis literal |
%const X |
n | any noun, no lift |
%arm X |
n | synonym for %const; intent: callable formula |
%crash |
— | [0 0] — Nock crash idiom |
%self |
— | [0 1] — whole subject |
%battery |
— | [0 2] — standard core battery |
%payload |
— | [0 3] — standard core payload |
%sample |
— | [0 6] — standard gate sample |
%context |
— | [0 7] — standard gate context |
%eval |
ff | both formulas |
%isa |
f | |
%inc |
f | |
%eq |
ff | |
%if |
fff | branches lift |
%comp |
ff | |
%push |
ff | |
%call N F |
af | |
%edit N V F |
aff | |
%hint T F |
nf | tag is a noun literal |
%hintd T C F |
nff | clue is a formula — per 4K spec it's evaluated |
The intent-marking opcodes (%arm, %crash, and the axis aliases) all lower
to the same cells as their %const / %slot equivalents — they exist purely
to surface meaning at the source level. %arm X is %const X for cases
where X is a formula that will later be invoked via %call; %self
through %context name the standard Hoon core/gate axes.
#let value and body are formulas. #match scrutinee and arm bodies are
formulas. Match patterns are noun literals (compared against the
scrutinee's evaluated value).
Raw cells [...] are taken structurally: their elements are not lifted.
That gives you an escape hatch into raw Nock when you need it, and the
cons-formula distribution pattern works as expected:
:subject {.a .b}
[(%inc .a) (%inc .b)]
; -> [[4 0 2] [4 0 3]]
; against [3 5] -> [4 6] via Nock distribution
Tests
The suites live in tests/; run them from the repo root.
python tests/test_nockasm.py # unit tests, 61 cases
python tests/test_e2e.py # end-to-end: expand -> pinochle -> verify, 19 cases
python tests/test_benchmarks.py # urbit/benchmark equivalents, 5 cases (loaded from disk)
python tests/test_hoon.py # hoon lib vs python oracle, 48 + 11 cases (urbit eval)
python tests/test_render.py # target-IR round-trip law + render idempotence
python tests/test_lift.py # jam/cue vectors + lift soundness, 69 cases
python tests/test_desk.py # on-ship test arms via urbit eval shim
python tests/test_mark.py # %nasm clay mark grow/grab round-trips
tests/test_benchmarks.py reads benchmarks/tests.json and benchmarks/<name>.nasm
from disk and runs each through pinochle. The five benchmarks present
(dec, add, factorial, fibonacci, ackermann) are faithful
transcriptions of urbit/benchmark/desk/bar/<name>.nock — each .nasm
expands to a noun bit-identical to the corresponding .nock formula.
License
MIT.
Release files for nockasm 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nockasm-1.3.0.tar.gz | 18.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nockasm-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.5 kB
Release files / nockasm-1.3.0.tar.gz
| Download URL | nockasm-1.3.0.tar.gz |
|---|---|
| Size | 18.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0004b6eabd69e771513f1222cdb43d2ddbac9c857e6a2d15e3ff34e9337ee71f
|
|
BLAKE2b-256 checksum How to use checksums |
984516226dd92ecd9211feaa17c5cf27db408ce0f3e974fd442e470de76e2bf1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Release files / nockasm-1.3.0-py3-none-any.whl
| Download URL | nockasm-1.3.0-py3-none-any.whl |
|---|---|
| Size | 20.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9d46ae3c6984c93456d1c5c2cfde81e177464ca99980d7156dcfbcb8434a2e08
|
|
BLAKE2b-256 checksum How to use checksums |
8034044db74bae7049b97cb09f9ece35d76f74b4d1fb42e2cfec6b080f67f004
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|