Metacircular Virtualization &
Practical Nock Interpretation

~lacnes

Abstract

This paper presents a detailed examination of metacircular virtualization in the Nock combinator calculus, focusing on the practical implementation of a virtualized Nock interpreter within the Urbit ecosystem. We explore the design and functionality of key components, including the ++mink interpreter, and their roles in enabling robust Nock execution environments. Extensions of Nock using fake opcodes, particularly opcode 12, are discussed, and a minimal working example of a Nock-virtualized Nock formula is provided.

Contents

1 Introduction
2 Background
3 Virtualized Nock
3.1 Crash handling
3.2 Language extension
3.3 Opcode 0
3.4 Opcode 1
3.5 Opcode 2
3.6 Opcode 3
3.7 Opcode 4
3.8 Opcode 5
3.9 Opcode 6
3.10 Opcode 7
3.11 Opcode 8
3.12 Opcode 9
3.13 Opcode 10
3.14 Opcode 11
3.15 Opcode 12
3.16 Et cetera
4 Nock in Nock
5 Conclusion
References

1 Introduction

The Nock combinator calculus is used as a target instruction set architecture (isa) for the Hoon and Jock languages. However, Nock itself is a “crash-only” paradigm which has no error handling mechanism within the terms of the isa itself. Furthermore, sometimes information is necessary to complete a calculation which may not be present in the apparent subject. Practical Nock interpreters handle this by running Nock within Nock, a process of metacircular virtualization. This article explores the design of ++mock, the basic virtualized Nock interpreter, and its role within a Nock-based kernel and standard library.

2 Background

In 1958, John McCarthy began his lifelong work on the Lisp programming language (McCarthy, 1996). While the form would evolve over the years, the fundamental components included S-expressions, or parenthesized lists of homoiconic code–data; ’ or quote, a deferred expression; and eval, a function to evaluate quoted code.1 Together these components allowed McCarthy and colleague Steve Russell to implement a Lisp interpreter in Lisp itself, the first metacircular interpreter (McCarthy and Wexelblat, 1978). Such was a radical departure from the imperative programming paradigm of the time, exemplified in Fortran (1954–55); Lisp’s reliance on recursion and higher-order functions was both a precursor to the functional program paradigm and a rejuvenation of the Church and Curry schools of fundamental computing logic.

Metacircularity was considered sufficiently important that several programming languages implemented it as a first-class feature, including Forth, TeX, and Prolog. They have generally been used either for tightly interpreted features like debuggers and code introspection tools, or for language extensions like macros.

Amin and Rompf (2017) analyzed how metacircular interpreters build on top of each other, each adding a new layer of abstraction. While primarily considering a Lisp dialect, they showed how interpreted language extensions (even in cascading towers) can be collapsed to bottom-level interpretation, similar to Nock’s own ++mink function. They epitomized a line of research on reflective languages and self-interpreted languages, such as quasiquotation in Lisp (Bawden, 1999) and Template Meta-Haskell (Sheard and Jones, 2002). The ultimate point of this enquiry was to show that metacircularity was a powerful and well-grounded tool for building interpreters, and that it could be used to in fact build practical interpreters.

Before Nock was even christened, one of its earliest design criteria was to straightforwardly implement a metacircular interpreter (~sorreg-namtyv, 2006; ~sorreg-namtyv, 2010). While the original motivation was to implement a hyper-Turing operator over a referentially transparent bound namespace, Nock’s support for virtualization also soon proved useful for managing error traces and crashes. Yarvin wrote in an early document:

Metacircularity in most languages is a curiosity and corner case. Programmers are routinely warned not to use it, and for good reason. Practical metacircularity is a particular strength of Nock, or any functional assembly language. Dynamic loading and/or virtualization works, at least logically, as in an OS.

We hypothesize that a metacircular functional dissemination protocol can serve as a complete, general-purpose system software environment—there is no problem it cannot solve, in theory or in practice. Such a protocol can be described as an “operating function” or OF—the functional analog of an imperative operating system. (~sorreg-namtyv, 2010)

Metacircularity thus closely ties to the idea of a functional operating system. This is a theme that has been explored in other contexts, such as the Lisp machines and the Haskell ghc runtime system. For Nock, a virtualized interpreter is no mere corner case but a first-class component of the system, both for practical execution and for resource discovery.

3 Virtualized Nock

Given an argument for metacircularity, we can now examine how Nock is actually virtualized in practice. The Urbit standard library provides a metacircular Nock interpreter, ++mink, along with several other components for building virtualized Nock environments. Some essential components provided by vanilla Hoon for virtualizing Nock include:

Nock execution environments virtualize Nock for two primary reasons:

  1. Crash handling (provisional execution).

  2. Language extension (additional opcodes).

3.1 Crash handling

While virtualization necessarily incurs some overhead relative to more direct execution, it gives the Nock programmer a way to bail (crash), trace (log with debugging information), and bind (time-limit) a computation. In fact, stack traces are computed through virtualization, and so are guaranteed to be as correct as their implementation.

A Nock virtualization environment needs to distinguish at least three kinds of results:

  1. A successful result of computation, along with a valid Nock noun.

  2. A block, which indicates that the computation is not yet complete and should be continued.

  3. A halt or deterministic error, which indicates that the computation has failed and should not be continued.

These correspond to standard scry results (see “Opcode 12” below) in this order:

  1. [~ ~ noun], a successful result.

  2. ~, a result is not currently available (block).

  3. [~ ~], a result will never become available (halt).

Because Nock is untyped and we need to distinguish a “zero” from a “block”, the structure of each scry result fundamentally differs from the other options.

Nondeterministic errors—crashes in the runtime, such as an out-of-memory error—are not handled by or visible to the Nock interpreter as a solid-state computer.

3.2 Language extension

Virtualization enables the extension of Nock using “fake opcodes”. At the current time, only opcode 12 has been added;2 this opcode provides a generalized way to supply the OS context to the scope in a way that appears like a GOTO or remote call but actually preserves Nock semantics including strict subject scoping. There is a sense in which the scry operation and the bound (referentially transparent) scry namespace are identical to the metacircularity of the Nock interpreter itself. The operating function itself can be treated as a first-class member of its own ecosystem.

Hoon defines +$nock already suited for virtualized Nock execution including fake opcode 12 (Listing 1).

Listing 1: Hoon’s +$nock specification
::            ::::::  virtual nock 
+$  nock  $^  ::  autocons 
              [p=nock q=nock] 
          $%  ::  constant 
5              [%1 p=*] 
              ::  compose 
              [%2 p=nock q=nock] 
              ::  cell test 
              [%3 p=nock] 
10              ::  increment 
              [%4 p=nock] 
              ::  equality test 
              [%5 p=nock q=nock] 
              ::  if, then, else 
15              [%6 p=nock q=nock r=nock] 
              ::  serial compose 
              [%7 p=nock q=nock] 
              ::  push onto subject 
              [%8 p=nock q=nock] 
20              ::  select arm and fire 
              [%9 p=@ q=nock] 
              ::  edit 
              [%10 p=[p=@ q=nock] q=nock] 
              ::  hint 
25              [%11 p=$@(@ [p=@ q=nock]) q=nock] 
              ::  grab data from sky 
              [%12 p=nock q=nock] 
              ::  axis select 
              [%0 p=@] 
30          ==

However, while ++mink is compiled to and evaluated in Nock, it is written in Hoon and accordingly uses Hoon’s affordances, particularly vases. Vases are a pair of type and data intended to store relevant type information for correctly evaluating raw untyped Nock nouns.3 Accordingly, it is Hoon’s higher-level introspective tools that facilitate ++mink’s operation.

The remainder of this section analyzes how each ++mink implements each Nock opcode, including particularly the fake opcode 12. The following code samples are taken from /sys/hoon in the Urbit codebase.

++mink produces a head atom marking the kind of result the interpreter has raised: if a %0, the result is a valid Nock noun; if a %1, a block is indicated (i.e. the halting problem continues); if a %2, then a deterministic error has occurred with an error trace.

3.3 Opcode 0

Opcode 0 is the identity function, which returns its argument unchanged. This is implemented in ++mink as follows:

:: *[a 0 b]      /[b a] 
  [%0 axis=@] 
=/  part  (frag axis.formula subject) 
?~  part  [%2 trace] 
5[%0 u.part]

In essence, an axis is provisionally extracted from the subject; if this is ~ then an error is raised with the current execution trace, else the resulting value from that axis is produced.

3.4 Opcode 1

Opcode 1 is the constant function, which returns a constant value extricated from the formula. This is implemented in ++mink as follows:

:: *[a 1 b]      b 
  [%1 constant=*] 
[%0 constant.formula]

3.5 Opcode 2

Opcode 2 is the evaluation function, which evaluates the formula on the subject. This is implemented in ++mink as follows:

:: *[a 2 b c]    *[*[a b] *[a c]] 
  [%2 subject=* formula=*] 
=/  subject  $(formula subject.formula) 
?.  ?=(%0 -.subject)  subject 
5=/  formula  $(formula formula.formula) 
?.  ?=(%0 -.formula)  formula 
%=  $ 
  subject  product.subject 
  formula  product.formula 
10==

Errors are passed directly through, whereas the resulting formula is invoked on the resulting subject.

3.6 Opcode 3

Opcode 3 is the cell test function, which tests whether the subject is a cell. This is implemented in ++mink as follows:

:: *[a 3 b]      ?*[a b] 
  [%3 subject=*] 
=/  argument  $(formula argument.formula) 
?.  ?=(%0 -.argument)  argument 
5[%0 .?(product.argument)]

This reduces to an evaluation of a base-level opcode 3 after a preliminary check that the formula evaluates correctly.

3.7 Opcode 4

Opcode 4 is the increment function, which increments the subject. This is implemented in ++mink as follows:

:: *[a 4 b]      +*[a b] 
  [%4 subject=*] 
=/  argument  $(formula argument.formula) 
?.  ?=(%0 -.argument)  argument 
5?^  product.argument  [%2 trace] 
[%0 .+(product.argument)]

Note that like opcode 3, this reduces to an evaluation of a base-level opcode 4.

3.8 Opcode 5

Opcode 5 is the equality test function, which tests whether the subject and formula are equal. This is implemented in ++mink as follows:

:: *[a 5 b c]    =[*[a b] *[a c]] 
  [%5 subject=* formula=*] 
=/  a  $(formula a.formula) 
?.  ?=(%0 -.a)  a 
5=/  b  $(formula b.formula) 
?.  ?=(%0 -.b)  b 
[%0 =(product.a product.b)]

Like opcodes 3 and 4, this reduces to an evaluation of a base-level opcode 5 after checking the left-hand and right-hand formulas.

3.9 Opcode 6

Opcode 6 is the if-then-else or conditional branch function, which evaluates the formula on the subject and then evaluates either the first or second formula depending on the result. This is implemented in ++mink as follows:

:: *[a 6 b c d]  *[a *[[c d] 0 *[[2 3] 0 *[a 4 4 b]]]] 
  [%6 test=* yes=* no=*] 
=/  result  $(formula test.formula) 
?.  ?=(%0 -.result)  result 
5?+  product.result 
      [%2 trace] 
  %&  $(formula yes.formula) 
  %|  $(formula no.formula) 
==

As with the direct opcode 6, there is no short-circuit evaluation in the conditional statement.

3.10 Opcode 7

Opcode 7 is the composition function, which evaluates the formula on the subject and then evaluates the next formula on the result. This is implemented in ++mink as follows:

:: *[a 7 b c]            *[*[a b] c] 
  [%7 subject=* next=*] 
=/  subject  $(formula subject.formula) 
?.  ?=(%0 -.subject)  subject 
5%=  $ 
  subject  product.subject 
  formula  next.formula 
==

This implementation simply drops the evaluation of subject through as the explicit subject for the next formula.

3.11 Opcode 8

Opcode 8 is the variable push function, which pushes a value onto the subject. This is implemented in ++mink as follows:

:: *[a 8 b c]            *[[*[a b] a] c] 
  [%8 head=* next=*] 
=/  head  $(formula head.formula) 
?.  ?=(%0 -.head)  head 
5%=  $ 
  subject  [product.head subject] 
  formula  next.formula 
==

Here the conceptual differences between opcode 7 and opcode 8 can be sharply compared: how 7 requires an evaluation against the current subject, leading to something of the nature of \(c(b(a))\) in terms of function composition, versus how 8 simply augments the subject.

3.12 Opcode 9

Opcode 9 is the arm select function, which selects an arm of the subject and evaluates it. This is implemented in ++mink as follows:

:: *[a 9 b c]    *[*[a c] 2 [0 1] 0 b] 
  [%9 axis=@ core=*] 
=/  core  $(formula core.formula) 
?.  ?=(%0 -.core)  core 
5=/  arm  (frag axis.formula product.core) 
?~  arm  [%2 trace] 
%=  $ 
  subject  product.core 
  formula  u.arm 
10==

For all the mechanics involved in opcode 9, the implementation is perhaps surprisingly simple. Extract the arm at the axis and evaluate it structurally using an opcode 2. (As an aside, Hoon which compiles to Nock aggressively utilizes lambdas to define functions at the point of use, and frequently employs a gate-building gate pattern. This latter resolves to two opcodes 9 in series.)

3.13 Opcode 10

Opcode 10 is the edit function, which edits a value in the subject at the given axis. This is implemented in ++mink as follows:

:: *[a 10 [b c] d]  #[b *[a c] *[a d]] 
  [%10 [axis=@ value=*] target=*] 
?:  =(0 axis.formula)  [%2 trace] 
=/  target  $(formula target.formula) 
5?.  ?=(%0 -.target)  target 
=/  value  $(formula value.formula) 
?.  ?=(%0 -.value)  value 
=/  mutant=(unit *) 
  (edit axis.formula product.target product.value) 
10?~  mutant  [%2 trace] 
[%0 u.mutant]

++edit traverses the noun to attempt to access a given axis; if it succeeds, it returns a new noun with the original value at that axis replaced by a new target value. If it fails, it returns an empty unit, indicating without a crash that the edit failed.

There’s more plumbing here because of the mutation of the noun, but the essential concept of locating and replacing a valid subtree is straightforward.

3.14 Opcode 11

Opcode 11 is the hint function, which provides a hint to the interpreter about how to interpret the argument. This is the only way to produce side effects from Nock as a pure function, and ++mink respects and passes through hints raised by the virtualized Nock formula. This is implemented in ++mink as follows:

:: *[a 11 b c]      *[a c] 
:: *[a 11 [b c] d]  *[[*[a c] *[a d]] 0 3] 
  [%11 tag=@ next=*] 
=/  next  $(formula next.formula) 
5?.  ?=(%0 -.next)  next 
:-  %0 
.*  subject 
[11 tag.formula 1 product.next] 
:: 
10  [%11 [tag=@ clue=*] next=*] 
=/  clue  $(formula clue.formula) 
?.  ?=(%0 -.clue)  clue 
=/  next 
  =?    trace 
15      ?=(?(%hunk %hand %lose %mean %spot) tag.formula) 
    [[tag.formula product.clue] trace] 
  $(formula next.formula) 
?.  ?=(%0 -.next)  next 
:-  %0 
20.*  subject 
[11 [tag.formula 1 product.clue] 1 product.next]

Two options match for opcode 11, a static and a dynamic hint. Even though this virtualized code “sandboxes” execution, the Nock formula including the clue is in fact evaluated. The runtime listens for a clue, which is compared to a whitelisted set of supported hints, as in normal (nonvirtualized) Nock execution.

Note that the argument cannot simply be discarded by the interpreter, because it may result in a crash. Thus, as with direct Nock execution, the hint must be evaluated.4

3.15 Opcode 12

Opcode 12 is the scry function, which retrieves a value from the sky namespace environment.5 This is implemented in ++mink as follows:

  [%12 ref=* path=*] 
=/  ref  $(formula ref.formula) 
?.  ?=(%0 -.ref)  ref 
=/  path  $(formula path.formula) 
5?.  ?=(%0 -.path)  path 
=/  result  (scry product.ref product.path) 
?~  result 
  [%1 product.path] 
?~  u.result 
10  [%2 [%hunk product.ref product.path] trace] 
[%0 u.u.result]

Opcode 12 does not have a direct Nock expression since it “steps outside” the subject–formula pair via the expedient of the scry gate.

In order to interpret opcode 12, ++mink takes as an argument, in addition to the subject and formula, a gate which will be invoked with the results of the subformulas of 12 as a sample. The result of executing this gate (not as interpreted by ++mink but by whichever interpreter is running ++mink) are then treated as the result of the Nock 12 operation. (“4n ++mink”, Urbit documentation)

Much of consequence is entailed by the use of a scry gate, which is a Hoon function that retrieves a value from an associated sky namespace (nominally, the bound scry namespace, although this is not necessarily entailed by Urbit in contemporary practice). The scry function bears the signature

$-(^ (unit (unit)))

The result of this operation is either a noun or a block, and it is returned as a (unit (unit noun)). These correspond to standard scry results in this order:

  1. [~ ~ noun], a successful result.

  2. ~, a result is not currently available (block).

  3. [~ ~], a result will never become available (halt).

For instance, the reference roof function for Arvo, the Urbit kernel, is schematically defined as:

++  roof 
  |$  [vase] 
  $-  $:  lyc=(unit (set ship))     ::  leakset 
          pov=path                  ::  provenance 
5          [vis=view bem=beam]       ::  perspective 
      ==                            :: 
  %-  unit                          ::  ~      unknown 
  %-  unit                          ::  ~ ~    invalid 
  (pair mark vase)                  ::  envased result 
10+$  view  $@(term [way=term car=term]) ::  perspective

An excursus is here warranted. The Arvo core is the Nock-based kernel of Urbit, and it is responsible for managing the state of the system and providing a consistent interface for agents to interact with that state. The Arvo core is built on top of the Nock interpreter, and it uses the ++mink function to interpret Nock formulas. Each vane in the Arvo core receives a +$roof in its function call, which it can call when it needs to scry with opcode 12. This gate provides access to the vane’s state, and it is used to retrieve values from the vane’s state in a way that is consistent with the Nock semantics.

++look is a gate that is used to convert the ++mink-compatible version of the scry handler into a version that can be used by the Arvo core. This gate acts as a bridge between the .^ dotket and the scry handler (++look) with access to the roof that came from the vane. This pattern imposes constraints on interpreters, but provides a way to mediate access to the state of the system in a way that is consistent with Nock semantics. Thus .^ dotket gives mediated access into the sky without exposing the state of the entire system in the subject (untyped permissions-free access).

3.16 Et cetera

++mink also features some machinery to correctly implement the cell distribution rule.

  [^ *] 
=/  head  $(formula -.formula) 
?.  ?=(%0 -.head)  head 
=/  tail  $(formula +.formula) 
5?.  ?=(%0 -.tail)  tail 
[%0 product.head product.tail]

++mink is naturally jetted for efficiency, ultimately by u3m_soft_run() in Urbit’s Vere (C) and by interpret() in NockApp’s NockVM (formerly Sword) (Rust), both the virtualization context for their respective runtimes. Some internal affordances for the runtime are in place to keep execution consistent; e.g., scry gates are stored on a stack to prevent re-entry if it scries.

Nock is a crash-only language, which means that when something goes wrong, the exception is an exception for the runtime or virtual machine to handle as it will. Typically, for a runtime this means an injection of the error trace back into the Nock kernel (cf. section “u3n: nock execution” in u3.md in the Urbit documentation6 ). (For ++mink, errors are attempted to be front-run by the interpreter and thus bad Nock should not be executed as such.)

4 Nock in Nock

Hearkening back to Amin and Rompf’s collapsed towers of interpreters, the affordances of ++mock as a metacircular interpreter permit us to build a tower of interpreters on top of it without incurring excessive performance overhead. As the author of u3.md concluded for the Vere C runtime, “we simply treat Nock proper as a special case of mock.” The conventional ++mink gate is built in Hoon, but what if we stripped it down and built it in Nock itself with the minimal necessary machinery? What would such an interpreter look like, and how would it relate to actual operation on a runtime in practice?

Let us extend ++mink to implement a logical loobean operator. (Call the modified gate ++pink.) This operation will be invoked with a new fake opcode 13. Under the hood, opcode 13 will simply logically negate a value (via an opcode 6).

:: *[a 13 b]                    NOT *[a b] 
  [%13 subject=*] 
=/  argument  $(formula argument.formula) 
?.  ?=(%0 -.argument)  [%2 trace] 
5?^  product.argument  [%2 trace] 
?:  =(%1 product.argument)  [%0 %.y] 
?:  =(%0 product.argument)  [%0 %.n] 
[%2 trace]

This is invoked as:

> (pink [0 13 1 0] *$-(^ (unit (unit)))) 
[%0 product=0] 
> (pink [0 13 1 1] *$-(^ (unit (unit)))) 
[%0 product=1] 
5> (pink [0 13 1 2] *$-(^ (unit (unit)))) 
[%2 trace=~]

Finally, it is instructive to consider what the minimal required amount of code machinery would be for one to successfully implement a Nock metacircular interpreter in Nock itself. An examination of ++mink shows modest reliance on Hoon’s higher-level features; tree math, for instance, in ++cap for distinguishing head vs. tail and ++mas for finding the relative address of an axis. (Neither of these are particularly complicated, but they entail some basic arithmetic operations as well.) By omitting ++scry and opcode 12, as well as reducing to a structural match with a +$tone instead of an explicit Hoon type match, one could produce a reasonably sized Hoon program compiling to something close to the minimal viable Nock virtualized interpreter. No doubt intrepid code golfers will find it very much an upper bound rather than a lower.

This Nock formula produces a working virtualized Nock interpreter derived from Hoon code with some hand tuning. The input function signature is [subject=* formula=*] with output of the form [@ product=*]; if the head is %0, then the product is a valid Nock noun, while a head of %bail indicates a crash in the evaluation. Opcode 9 calls may be addressed into the core as subject, and the formula itself is complete with an empty subject.

[[1 
  [:: +mul multiplication of two atoms 
   8 [1 1 1] 
     [1 8 [1 0] 
5        8 [1 6 [5 [1 0] 0 60] 
               [0 6] 
               9 2 10 [60 8 [9 686 0 31] 
                             9 2 10 [6 0 124] 0 2] 
                   10 [6 8 [9 20 0 31] 
10                             9 2 10 [6 [0 125] 0 14] 
                       0 2] 0 1] 
      9 2 0 1] 0 1] 
  [[:: +add addition of two atoms 
    8 [1 0 0] 
15      [1 6 [5 [1 0] 0 12] 
           [0 13] 
           9 2 10 
           [6 [8 [9 686 0 7] 9 2 10 [6 0 28] 0 2] 
            4 0 13] 
20         0 1] 0 1] 
     [[8 
       :: +list type definition 
        [8 [1 0] 
           [1 8 [0 6] 8 [5 [0 14] 0 2] 0 6] 
25         0 1] 
        [1 8 [1 0] 
          [1 8 
             [6 [3 0 6] 
                [[6 [5 [1 0] 0 12] [1 0] 0 0] 
30                 8 [0 30] 9 2 10 [6 0 29] 0 2] 
              6 [5 [1 0] 0 6] [1 0] 0 0] 
            8 [5 [0 14] 0 2] 0 6] 
          0 1] 0 1] 
      [:: +dvr division with remainder 
35       8 [1 1 1] 
         [1 6 [5 [1 0] 0 13] 
              [0 0] 
              8 [1 0] 
              8 [1 6 [8 [9 687 0 31] 
40                      9 2 10 [6 [0 124] 0 125] 0 2] 
                 [[0 6] 0 60] 
                 9 2 10 
                 [60 8 [9 23 0 31] 
                       9 2 10 [6 [0 124] 0 125] 0 2] 
45                 10 [6 4 0 6] 0 1] 
           9 2 0 1] 0 1] 
       [:: +cap: 
        8 [1 0] 
          [1 6 [5 [1 2] 0 6] 
50               [1 2] 
               6 [5 [1 3] 0 6] 
                 [1 3] 
                 6 [6 [5 [1 1] 0 6] [1 0] 5 [1 0] 0 6] 
                   [0 0] 
55                   9 2 10 
                   [6 7 [8 [9 170 0 7] 
                            9 2 10 
                            [6 [0 14] 7 [0 3] 1 2] 
                         0 2] 0 2] 
60           0 1] 0 1] 
       [:: +dec decrement an atom 
        8 [1 0] 
          [1 6 [5 [1 0] 0 6] 
               [0 0] 
65               8 [1 0] 
               8 [1 6 [5 [0 30] 4 0 6] 
                      [0 6] 
                      9 2 10 [6 4 0 6] 0 1] 
           9 2 0 1] 0 1] 
70       :: +lth less-than test 
       8 [1 0 0] 
         [1 6 [6 [5 [0 12] 0 13] [1 1] 1 0] 
              [6 [8 
                  [1 6 [5 [1 0] 0 28] 
75                       [1 0] 
                       6 [6 [6 [5 [1 0] 0 29] 
                               [1 1] 
                               1 0] 
                            [6 [9 2 10 
80                                [14 [8 [9 686 0 15] 
                                     9 2 10 [6 0 60] 
                                     0 2] 
                                 8 [9 686 0 15] 
                                 9 2 10 [6 0 61] 0 2] 
85                                0 1] 
                               [1 0] 
                               1 1] 
                           1 1] 
                    [1 0] 1 1] 9 2 0 1] 
90           [1 0] 1 1] 1 1] 0 1] 
     [:: +edit edit an atom at a given axis 
      8 [1 0 0 0] 
        [1 6 [5 [1 1] 0 12] 
             [[1 0] 0 27] 
95             6 [6 [3 0 26] [1 1] 1 0] 
               [1 0] 
               8 [9 2 10 [26 
                          8 [8 [9 342 0 63] 
                               9 2 10 [6 0 28] 0 2] 
100                          6 [5 [1 2] 0 2] 
                            [0 116] 
                            6 [5 [1 3] 0 2] 
                              [0 117] 
                              1 1.818.845.538 0] 
105                            10 [12 8 [9 87 0 63] 
                                9 2 10 [6 0 28] 0 2] 
                   0 1] 
                 6 [5 [1 0] 0 2] 
                   [1 0] 
110                   8 [8 [9 342 0 15] 
                        9 2 10 [6 0 60] 0 2] 
                     6 [5 [1 2] 0 2] 
                       [7 [0 3] [1 0] [0 5] 0 117] 
                       6 [5 [1 3] 0 2] 
115                         [7 [0 3] [1 0] [0 116] 0 5] 
                         1 1.818.845.538 0] 
      0 1] 
     [:: +jink, raw virtual Nock 
      8 [1 0 0] 
120        [1 8 
           6  :: Cell distribution 
           [6 [3 0 13] [3 0 26] 1 1] 
           [8 [9 2 10 [13 0 26] 0 1] 
             6 [5 [1 0] 0 4] 
125               [8 [9 2 10 [13 0 59] 0 3] 
                 6 [5 [1 0] 0 4] 
                   [[1 0] [0 13] 0 5] 
                0 2] 
            0 2] 
130           6  :: Opcode 0 
           [6 [3 0 13] 
              [6 [5 [1 0] 0 26] 
                 [6 [3 0 27] [1 1] 1 0] 
                 1 1] 
135              1 1] 
           [8 [8 [9 22 0 7] 
                 9 2 10 [6 [0 59] 0 28] 0 2] 
              6 [5 [1 0] 0 2] 
                [1 1.818.845.538 0] 
140                [1 0] 
              0 5] 
           6  :: Opcode 1 
           [6 [3 0 13] 
              [5 [1 1] 0 26] 1 1] 
145           [[1 0] 0 27] 
           6  :: Opcode 2 
           [6 [3 0 13] 
              [6 [5 [1 2] 0 26] [3 0 27] 1 1] 1 1] 
           [8 [9 2 10 [13 0 54] 0 1] 
150              6 [5 [1 0] 0 4] 
                [8 [9 2 10 [13 0 119] 0 3] 
                   6 [5 [1 0] 0 4] 
                     [9 2 10 [6 [0 13] 0 5] 0 7] 
                     0 2] 
155            0 2] 
           6  :: Opcode 3 
           [6 [3 0 13] 
              [5 [1 3] 0 26] 1 1] 
           [8 [9 2 10 [13 0 27] 0 1] 
160            6 [5 [1 0] 0 4] 
              [[1 0] 3 0 5] 
              0 2] 
           6  :: Opcode 4 
           [6 [3 0 13] 
165              [5 [1 4] 0 26] 
              1 1] 
           [8 [9 2 10 [13 0 27] 0 1] 
              6 [5 [1 0] 0 4] 
                [6 [6 [3 0 5] [1 1] 1 0] 
170                   [[1 0] 4 0 5] 
                   1 1.818.845.538 0] 
            0 2] 
           6  :: Opcode 5 
           [6 [3 0 13] 
175              [6 [5 [1 5] 0 26] [3 0 27] 1 1] 1 1] 
           [8 [9 11 10 [29 0 118] 0 1] 
              6 [5 [1 0] 0 4] 
                [8 [9 2 10 [13 0 119] 0 3] 
                 6 [5 [1 0] 0 4] 
180                   [[1 0] 5 [0 13] 0 5] 
                   0 2] 
            0 2] 
           6  :: Opcode 6 
           [6 [3 0 13] 
185              [6 [5 [1 6] 0 26] 
                 [6 [3 0 27] [3 0 55] 1 1] 1 1] 1 1] 
           [8 [9 11 10 [29 0 118] 0 1] 
              6 [5 [1 0] 0 4] 
                [6 [5 [1 0] 0 5] 
190                   [9 2 10 [13 0 238] 0 3] 
                   6 [5 [1 1] 0 5] 
                     [9 2 10 [13 0 239] 0 3] 
                     1 1.818.845.538 0] 
             0 2] 
195           6  :: Opcode 7 
           [6 [3 0 13] 
              [6 [5 [1 7] 0 26] [3 0 27] 1 1] 1 1] 
           [8 [9 2 10 [13 0 54] 0 1] 
              6 [5 [1 0] 0 4] 
200                [9 2 10 [6 [0 5] 0 119] 0 3] 
                0 2] 
           6  :: Opcode 8 
           [6 [3 0 13] 
              [6 [5 [1 8] 0 26] [3 0 27] 1 1] 1 1] 
205           [8 [9 2 10 [13 0 54] 0 1] 
              6 [5 [1 0] 0 4] 
                [9 2 10 [6 [[0 5] 0 28] 0 119] 0 3] 
                0 2] 
           6  :: Opcode 9 
210           [6 [3 0 13] 
              [6 [5 [1 9] 0 26] 
                 [6 [3 0 27] 
                    [6 [3 0 54] [1 1] 1 0] 
                    1 1] 
215                 1 1] 
              1 1] 
           [8 [9 2 10 [13 0 55] 0 1] 
              6 [5 [1 0] 0 4] 
                [8 [8 [9 22 0 15] 
220                      9 2 10 [6 [0 246] 0 13] 0 2] 
                6 [5 [1 0] 0 2] 
                  [1 1.818.845.538 0] 
               9 2 10 [6 [0 13] 0 5] 
               0 7] 
225             0 2] 
           6  :: Opcode 10 
           [6 [3 0 13] 
              [6 [5 [1 10] 0 26] 
                 [6 [3 0 27] 
230                    [6 [3 0 54] 
                       [6 [3 0 108] [1 1] 1 0] 
                       1 1] 
                    1 1] 
                 1 1] 
235              1 1] 
           [6 [5 [1 0] 0 108] 
              [1 1.818.845.538 0] 
              8 [9 2 10 [13 0 55] 0 1] 
              6 [5 [1 0] 0 4] 
240                [8 [9 2 10 [13 0 237] 0 3] 
                6 [5 [1 0] 0 4] 
                  [8 [8 [9 86 0 31] 
                        9 2 10 
                        [6 [0 1.004] [0 29] 0 13] 
245                      0 2] 
                 6 [5 [1 0] 0 2] 
                   [1 1.818.845.538 0] 
                   [1 0] 
                 0 5] 
250                0 2] 
              0 2] 
           6  :: Opcode 11 
           [6 [3 0 13] 
              [6 [5 [1 11] 0 26] 
255                 [6 [3 0 27] 
                    [6 [3 0 54] 
                       [1 1] 
                       1 0] 
                    1 1] 
260                 1 1] 
              1 1] 
           [8 [9 2 10 [13 0 55] 0 1] 
              6 [5 [1 0] 0 4] 
                [[1 0] 
265                 2 [0 28] [1 11] [0 118] [1 1] 0 5] 
             0 2] 
           1 1.818.845.538 0] 
         0 1] 
     :: +mas 
270     8 [1 0] 
       [1 6 [6 [5 [1 3] 0 6] [1 0] 5 [1 2] 0 6] 
            [1 1] 
            6 [6 [5 [1 1] 0 6] [1 0] 5 [1 0] 0 6] 
              [0 0] 
275              8 [9 20 0 7] 
              9 2 10 
              [6 [7 [0 3] 7 [8 [9 170 0 7] 
                             9 2 10 
                             [6 [0 14] 7 [0 3] 1 2] 
280                             0 2] 0 3] 
         7 [0 3] 8 [9 4 0 7] 
         9 2 10 [6 
           [7 [0 3] 
             9 2 10 
285             [6 7 [8 [9 170 0 7] 
                   9 2 10 [6 [0 14] 7 [0 3] 1 2] 
                   0 2] 0 2] 
             0 1] 
           7 [0 3] 1 2] 
290         0 2] 
       0 2] 
     0 1] 
   [:: +frag fragmentary subtree of noun 
    8 [1 0 0] 
295      [1 6 [5 [1 0] 0 12] 
           [1 0] 
           8 [1 6 [5 [1 1] 0 28] 
                  [[1 0] 0 29] 
                  6 [6 [3 0 29] [1 1] 1 0] 
300                    [1 0] 
                    9 2 10 
                    [14 [8 [9 175 0 15] 
                         9 2 10 [6 0 60] 0 2] 
                     8 [8 [9 342 0 15] 
305                        9 2 10 [6 0 60] 0 2] 
                     6 [5 [1 2] 0 2] 
                       [0 122] 
                       6 [5 [1 3] 0 2] 
                       [0 123] 
310                       1 1.818.845.538 0] 
              0 1] 
       9 2 0 1] 
    0 1] 
   [:: +unit, type definition of unit/Maybe 
315    8 
     [8 [1 0] [1 8 [0 6] 8 [5 [0 14] 0 2] 0 6] 0 1] 
     [1 8 [1 0] 
          [1 8 [6 [3 0 6] 
                  [[8 [0 30] 9 2 10 [6 0 28] 0 2] 
320                    8 [7 [0 7] 8 [9 46 0 7] 
                       9 2 10 [6 0 14] 0 2] 
                   9 2 10 [6 0 29] 0 2] 
                  6 [5 [1 0] 0 6] 
                    [1 0] 
325                    0 0] 
           8 [5 [0 14] 0 2] 
           0 6] 
       0 1] 
     0 1] 
330   :: +sub subtraction of two atoms 
   8 [1 0 0] 
     [1 6 [5 [1 0] 0 13] 
          [0 12] 
          9 2 10 
335          [6 [8 [9 686 0 7] 9 2 10 [6 0 28] 0 2] 
           8 [9 686 0 7] 9 2 10 [6 0 29] 0 2] 
     0 1] 
   0 1] 
 0 1]

5 Conclusion

Metacircular virtualization is a powerful tool for building interpreters, and Nock is not only no exception, it exemplifies the practice. The design of ++mock and its underlying ++mink function provides a practical way to interpret Nock in a way that is both efficient and extensible. One could imagine, for instance, a Nock interpreter cousin to ++mock that is instrumented for partial evaluation of formulas (as in a debugger or syntax highlighter robust to syntax errors). Nock supports a rich capacity to build metacircular interpreters as a first-class feature, a potential which remains as yet relatively unexplored in the Nock ecosystem. PIC

References

Amin, Nada and Tiark Rompf (2017). “Collapsing towers of interpreters.” In: Proceedings of the acm on Programming Languages. Vol. 2. popl. New York, NY: Association for Computing Machinery, pp. 1–33. url: https://doi.org/10.1145/3158140 (visited on ~2025.8.13).

Bawden, Alan (1999). “Quasiquotation in Lisp.” In: Proceedings of the 1999 acm sigplan workshop on Partial evaluation and semantics-based program manipulation. New York, NY: Association for Computing Machinery, pp. 4–12. url: https://dl.acm.org/doi/10.1145/317636.317640 (visited on ~2025.8.13).

Graham, Paul (2001). “What Made Lisp Different.” In: url: https://paulgraham.com/diff.html (visited on ~2025.8.13).

McCarthy, John (1996). History of Lisp. url: https://www-formal.stanford.edu/jmc/history/lisp/node2.html (visited on ~2025.8.13).

McCarthy, John and Richard L. Wexelblat (1978). “History of lisp.” In: History of programming languages. Ed. by Richard L. Wexelblat. New York, NY: Association for Computing Machinery, pp. 173–185. url: https://doi.org/10.1145/800025.1198360 (visited on ~2025.8.13).

Sheard, Tim and Simon Peyton Jones (2002). “Template meta-programming for Haskell.” In: Proceedings of the 2002 acm sigplan workshop on Haskell. New York, NY: Association for Computing Machinery, pp. 1–16. url: https://dl.acm.org/doi/10.1145/581690.581691 (visited on ~2025.8.13).

~sorreg-namtyv, Curtis Yarvin (2006) “U, a small model”. url: http://urbit.sourceforge.net/u.txt (visited on ~2024.2.20).

— (2010a) “Urbit: functional programming from scratch”. url: http://moronlab.blogspot.com/2010/01/urbit-functional-programming-from.html (visited on ~2024.1.25).

— (2010b) “Watt: A Self-Sustaining Functional Language (preprint)”. url: https://github.com/cgyarvin/urbit/blob/master/Spec/watt/sss10.tex (visited on ~2025.5.19).

Footnotes

1Paul Graham, in his 2001 short essay “What Made Lisp Different”, grouped these concepts into the phrase “The whole language always available” (2001).

2Compare, however, the approach of ~barpub-tarber, pp. 191–228 in this volume, who presented an explicitly constructive Nock-like paradigm in Ax.

3See ~lagrev-nocfep, pp. 131–153 in this volume, for a discussion of vases and their role in practical Nock compilation.

4Here, this should be read “evaluated safely”, with the caveat that some hints may result in a runtime crash. For instance, an ill-formed %slog hint (printf) will crash the interpreter if the clue is not structured correctly. This is a consequence of the fact that Nock is untyped and so the interpreter cannot know whether the hint is well-formed or not. The ++mink interpreter does not attempt to validate hints, but merely passes them through to the runtime for evaluation. (One could imagine a Nock virtualizer which itself handled certain hints as if they were “side effects” to the evaluation.)

5There is a sense in which opcodes 6–11 are also virtualized extensions to Nock, acting as they do as macros. See ~niblyx-malnus, pp. 47–70 in this volume.

6This section devoted a fair amount of attention to opcode 11 now 12 (it being written for Nock 5K) and hint evaluation.