~lagrev-nocfep / Sigilante / Neal Davis
The permanent version of this gist has moved to tutorials/jetting.md
.
The goals of this tutorial are for you to be able to:
- Read existing jet code.
- Produce a jet matching a Hoon gate with a single argument.
- Produce a more complex jet involving multiple values and floating-point arithmetic.
A fully-implemented version of the ++factorial
jet is available at natareo/urbit
.
- ~timluc-miptev, “Jets” (recommended to start here first)
- Tlon Corporation, “u3: Land of Nouns” (recommended as supplement to this document)
- Tlon Corporation, “API overview by prefix” (recommended as supplement after this document)
- Tlon Corporation, “Writing Jets” (largely superseded herein)
Given a Hoon gate, how can a developer produce a matching C jet? Let us
illustrate the process using a simple |%
core.
We assume the reader has achieved facility with both Hoon code and C
code. This tutorial aims to communicate the practical process of
producing a jet, and many
u3
noun concepts are only
briefly discussed or alluded to.
To this end, we begin by examining the Hoon ++add
gate, which accepts two values in its sample.
The Hoon code for ++add
decrements one of these values and adds one to the other for each decrement until zero is reached. This is because all atoms in Hoon are unsigned integers and Nock has no simple addition operation. The source code for ++add
is located in hoon.hoon
:
|%
+| %math
++ add
~/ %add
:: unsigned addition
::
:: a: augend
:: b: addend
|= [a=@ b=@]
:: sum
^- @
?: =(0 a) b
$(a (dec a), b +(b))
or in a more compact form (omitting the parent core and chapter label)
++ add
~/ %add
|= [a=@ b=@] ^- @
?: =(0 a) b
$(a (dec a), b +(b))
The jet hint %add
allows Hoon to hint to the runtime that a jet may exist. By convention, the jet hint name matches the gate label. Jets must be registered elsewhere in the runtime source code for the Vere binary to know where to connect the hint; we elide that discussion until we take a look at jet implementation below.
The following C code implements ++add
as a significantly faster operation including handling of >31-bit atoms. It may be found in urbit/pkg/urbit/jets/a/add.c
:
u3_noun
u3qa_add(u3_atom a,
u3_atom b)
{
if ( _(u3a_is_cat(a)) && _(u3a_is_cat(b)) ) {
c3_w c = a + b;
return u3i_words(1, &c);
}
else if ( 0 == a ) {
return u3k(b);
}
else {
mpz_t a_mp, b_mp;
u3r_mp(a_mp, a);
u3r_mp(b_mp, b);
mpz_add(a_mp, a_mp, b_mp);
mpz_clear(b_mp);
return u3i_mp(a_mp);
}
}
u3_noun
u3wa_add(u3_noun cor)
{
u3_noun a, b;
if ( (c3n == u3r_mean(cor, u3x_sam_2, &a, u3x_sam_3, &b, 0)) ||
(c3n == u3ud(a)) ||
(c3n == u3ud(b) && a != 0) )
{
return u3m_bail(c3__exit);
} else {
return u3qa_add(a, b);
}
}
The main entry point for a call into the function is u3wa_add
. u3w
functions are translator functions which accept the entire sample as a u3_noun
(or Nock noun). u3q
functions take custom combinations of nouns and atoms and generally correspond to unpacked samples.
u3wa_add
defines two nouns a
and b
which will hold the unpacked arguments from the sample. The sample elements are copied out by reference into a
from sample address 2 (u3x_sam_2
) and into b
from sample address 3 (u3x_sam_3
). A couple of consistency checks are made; if these fail, u3m_bail
yields a runtime error. Else u3qa_add
is invoked on the C-style arguments.
u3qa_add
has the task of adding two Urbit atoms. There is a catch, however! An atom may be a direct atom (meaning the value as an unsigned integer fits into 31 bits) or an indirect atom (anything higher than that). Direct atoms, called cat
s, are indicated by the first bit being zero.
0ZZZ.ZZZZ.ZZZZ.ZZZZ.ZZZZ.ZZZZ.ZZZZ.ZZZZ
Any atom value which may be represented as Z
bits simply contain the value.
> `@ub`2.147.483.647
0b111.1111.1111.1111.1111.1111.1111.1111
> `@ux`2.147.483.647
0x7fff.ffff
However, any atom with a value greater than this (including many cords, floating-point values, etc.) is an indirect atom (or dog
) marked with a prefixed bit of one.
11YX.XXXX.XXXX.XXXX.XXXX.XXXX.XXXX.XXXX
where bit 31 indicates indirectness, bit 30 is always set, and bit 29 (Y
) indicates if the value is an atom or a cell. An indirect atom contains a pointer into the loom from bits 0–28 (bits X
).
What does this mean for u3qa_add
? It means that if the atoms are both direct atoms (cat
s), the addition is straightforward and simply carried out in C. When converted back into an atom, a helper function u3i_words
deals with the possibility of overflow and the concomitant transformation to a dog
.
c3_w c = a + b; # c3_w is a 32-bit C word.
return u3i_words(1, &c);
There's a second trivial case to handle one of the values being zero. (It is unclear to the author of this tutorial why both cases as-zero are not being handled; the speed change may be too trivial to matter.)
Finally, the general case of adding the values at two loom addresses is dealt with. This requires general pointer-based arithmetic with GMP multi-precision integer operations.
mpz_t a_mp, b_mp; # mpz_t is a GMP multi-precision integer type
u3r_mp(a_mp, a); # read the atoms out of the loom into the MP type
u3r_mp(b_mp, b);
mpz_add(a_mp, a_mp, b_mp); # carry out MP-correct addition
mpz_clear(b_mp); # clear the now-unnecessary `b` value from memory
return u3i_mp(a_mp); # write the value back into the loom and return it
The procedure to solve the problem in the C jet does not need to follow the same algorithm as the Hoon code.
In general, jet code feels a bit heavy and formal. Jet code may call other jet code, however, so much as with Hoon layers of complexity can be appropriately encapsulated. Once you are used to the conventions of the u3 library, you will be in a good position to produce working and secure jet code.
Similar to how we encountered recursion way back in Hoon School to talk about gate mechanics, let us implement a C jet of the ++factorial
example code. We will call this library trig
in a gesture to some subsequent functions you should implement as an exercise. Create a file lib/trig.hoon
with the following contents:
~% %trig ..part ~
|%
:: Factorial, $x!$
::
++ factorial
~/ %factorial
|= x=@ud ^- @ud
=/ t=@ud 1
|- ^- @rs
?: =(x 0) t
?: =(x 1) t
$(x (sub x 1), t (mul t x))
--
We will create a generator gen/trig.hoon
which will help us quickly check the library's behavior.
/+ *trig
!:
:- %say
|= [[* eny=@uv *] [x=@rs n=@rs ~] ~]
::
~& (factorial n)
~& (absolute x)
~& (exp x)
~& (pow-n x n)
[%verb ~]
We will further define a few unit tests as checks on arm behavior in tests/lib/trig.hoon
:
/+ *test, *trig
::
::::
::
|%
++ test-factorial ^- tang
;: weld
%+ expect-eq
!> 1
!> (factorial 0)
%+ expect-eq
!> 1
!> (factorial 1)
%+ expect-eq
!> 120
!> (factorial 5)
%+ expect-eq
!> 720
!> (factorial 6)
==
--
Jet development can require frequent production of new Urbit binaries
and , so let us pay attention to our working affordances. We will
primarily work in the fakezod on the two files just mentioned, and in
the pkg/urbit
directory of the main Urbit repository, so we need a
development process that allows us to quickly access each of these, move
them into the appropriate location, and build necessary components. The
basic development cycle will look like this:
-
Compose correct Hoon code.
-
Hint the Hoon code.
-
Register the jets in the Vere C code.
-
Compose the jets.
-
Compile and troubleshoot.
-
Repeat as necessary.
You should consider using a terminal utility like tmux
or screen
which allows you to work in several locations on your file system
simultaneously: one for file system operations (copying files in and out
of the home
directory), one for running the fakezod, and one for
editing the files, or an IDE or text editor if preferred.
To start off, you should obtain a clean up-to-date copy of the Urbit
repository, available on GitHub at
github.com/urbit/urbit
. Make a
working directory ~/tlon
. Create a new branch within the repo named
trigjet
:
cd
mkdir tlon
cd tlon
git clone https://github.com/urbit/urbit.git
cd urbit
git branch trigjet
Test your build process to produce a local executable binary of Vere:
make
This invokes Nix to build the Urbit binary. Take note of where that
binary is located (typically in /tmp
on your main file system) and
create a new fakezod using a downloaded pill.
cd ..
wget https://bootstrap.urbit.org/urbit-v1.5.pill
<Nix build path>/bin/urbit -B urbit-v1.5.pill -F zod
Inside of that fakezod, sync %clay
to Unix,
|mount %
Then copy the entire home
desk out so that you can work with it and
copy it back in as necessary.
cp -r zod/home .
Save the foregoing library code in home/lib
and the generator code in
home/gen
; also, don't forget the unit tests! Whenever you work in your preferred editor, you should work on the home
copies, then move them back into the fakezod and synchronize before execution.
cp -r home zod
|commit %home
Now that you have a developer cycle in place, let's examine what's necessary to produce a jet. A jet is a C function which replicates the behavior of a Hoon (Nock) gate. Jets have to be able to manipulate Urbit quantities within the binary, which requires both the proper affordances within the Hoon code (the interpreter hints) and support for manipulating Urbit nouns (atoms and cells) within C.
Jet hints must provide a trail of symbols for the interpreter to know
how to match the Hoon arms to the corresponding C code. Think of these
as breadcrumbs. The current jetting
documentation demonstrates a
three-deep jet; here we have a two-deep scenario. Specifically, we mark
the outermost arm with ~%
and an explicit reference to the Arvo core
(the parent of part
). We mark the inner arms with ~/
because their
parent symbol can be determined from the context. The @tas
token will
tell Vere which C code matches the arm. All symbols in the nesting
hierarchy must be included.
~% %trig ..part ~
|%
++ factorial
~/ %factorial
|= x=@ud ^- @ud
...
--
We also need to add appropriate handles for the C code. This consists of several steps:
-
Register the jet symbols and function names in
tree.c
. -
Declare function prototypes in headers
q.h
andw.h
. -
Produce functions for compilation and linking in the
pkg/urbit/jets/e
directory.
The first two steps are fairly mechanical and straightforward.
Register the jet symbols and function names. A jet registration may
be carried out at in point in tree.c
. The registration consists of
marking the core
/* Jet registration of ++factorial arm under trig */
static u3j_harm _140_hex__trig_factorial_a[] = {{".2", u3we_trig_factorial, c3y}, {}};
/* Associated hash */
static c3_c* _140_hex__trig_factorial_ha[] = {
"903dbafb8e59427eced0b35379ad617c2eb6083a235075e9cdd9dd80e732efa4",
0
};
static u3j_core _140_hex__trig_d[] =
{ { "factorial", 7, _140_hex__trig_factorial_a, 0, _140_hex__trig_factorial_ha },
{}
};
static c3_c* _140_hex__trig_ha[] = {
"0bac9c3c43634bb86f6721bbcc444f69c83395f204ff69d3175f3821b1f679ba",
0
};
/* Core registration by token for trig */
static u3j_core _140_hex_d[] =
{ /* ... pre-existing jet registrations ... */
{ "trig", 31, 0, _140_hex__trig_d, _140_hex__trig_ha },
{}
};
The numeric component of the title, 140
, indicates the Hoon Kelvin
version. Library jets of this nature are registered as hex
jets,
meaning they live within the Arvo core. Other, more inner layers of
%zuse
and %lull
utilize pen
and other three-letter jet tokens. The
core is conventionally included here, then either a d
suffix for the
function association or a ha
suffix for a jet hash. (Jet hashes are a way of “signing” code. They are not as of this writing actively used by the binary runtimes.)
The particular flavor of C mandated by the Vere kernel is quite
lapidary, particularly when shorthand functions (such as u3z
) are
employed. In this code, we see the following u3
elements:
-
c3_c
, the platform C 8-bitchar
type -
c3y
, loobean true,%.y
(similarlyc3n
, loobean false,%.n
) -
u3j_core
, C representation of Hoon/Nock cores -
u3j_harm
, an actual C jet ("Hoon arm")
The numbers 7
and 31
refer to relative core addresses. In most
cases---unless you're building a particularly complicated jet or
modifying %zuse
or %lull
---you can follow the pattern laid out here.
".2"
is a label for the axis in the core [battery sample]
, so just
the battery. The text labels for the |%
core and the arm are included
at their appropriate points. Finally, the jet function entry point
u3we_trig_factorial
is registered.
For more information on u3, please check out the u3 summary below or the official documentation at “u3: Land of Nouns”.
Declare function prototypes in headers.
A u3w
function is always the entry point for a jet. Every u3w
function accepts a u3noun
(a Hoon/Nock noun), validates it, and
invokes the u3q
function that implements the actual logic. The u3q
function needs to accept the same number of atoms as the defining arm
(since these same values will be extricated by the u3w
function and
passed to it).
In this case, we have cited u3we_trig_factorial
in tree.c
and now
must declare both it and u3qe_trig_factorial
:
In w.h
:
u3_noun u3we_trig_factorial(u3_noun);
In q.h
:
u3_noun u3qe_trig_factorial(u3_atom);
Produce functions for compilation and linking.
Given these function prototype declarations, all that remains is the
actual definition of the function. Both functions will live in their own
file; we find it the best convention to associate all arms of a core in
a single file. In this case, create a file pkg/urbit/jets/e/trig.c
and
define all of your trig
jets therein. (Here we show ++factorial
only.)
As with ++add
, we have to worry about direct and indirect atoms when carrying out arithmetic operations, prompting the use of GMP mpz
operations.
/* jets/e/trig.c
**
*/
#include "all.h"
#include <stdio.h> // helpful for debugging, removable after development
/* factorial of @ud integer
*/
u3_noun
u3qe_trig_factorial(u3_atom a) /* @ud */
{
fprintf(stderr, "u3qe_trig_factorial\n\r"); // DELETE THIS LINE LATER
if (( 0 == a ) || ( 1 == a )) {
return 1;
}
else if ( _(u3a_is_cat(a))) {
c3_d c = ((c3_d) a) * ((c3_d) (a-1));
return u3i_chubs(1, &c);
}
else {
mpz_t a_mp, b_mp;
u3r_mp(a_mp, a);
mpz_sub(b_mp, a_mp, 1);
u3_atom b = u3qe_trigrs_factorial(u3i_mp(b_mp));
u3r_mp(b_mp, b);
mpz_mul(a_mp, a_mp, b_mp);
mpz_clear(b_mp);
return u3i_mp(a_mp);
}
}
u3_noun
u3we_trig_factorial(u3_noun cor)
{
fprintf(stderr, "u3we_trig_factorial\n\r"); // DELETE THIS LINE LATER
u3_noun a;
if ( c3n == u3r_mean(cor, u3x_sam, &a, 0) ||
c3n == u3ud(a) )
{
return u3m_bail(c3__exit);
}
else {
return u3qe_trig_factorial(a);
}
}
This code merits ample discussion. Without focusing on the particular types used, read through the logic and look for the skeleton of a standard simple factorial algorithm.
u3r
operations are used to extract Urbit-compatible types as C values.
u3i
operations wrap C values back into Urbit-compatible types.
Before proceeding to compose a more complicated floating-point jet, we should step back and examine the zoo of u3 functions that jets use to formally structure atom access and manipulation.
u3
defines a number of functions for extracting data from Urbit types
into C types for ready manipulation, then wrapping those same values
back up for Urbit to handle. These fall into several categories:
Prefix | Mnemonic | Source File | Example of Function |
---|---|---|---|
u3a_ |
Allocation | allocate.c |
u3a_malloc |
u3e_ |
Event (persistence) | events.c |
u3e_foul |
u3h_ |
Hash table | hashtable.c |
u3h_put |
u3i_ |
Imprisonment (noun construction) | imprison.c |
|
u3j_ |
Jet control | jets.c |
u3j_boot |
u3k_ |
Jets (transfer semantics, C arguments) | [a-g]/*.c |
|
u3l_ |
Logging | log.c |
u3l_log |
u3m_ |
System management | manage.c |
u3m_bail |
u3n_ |
Nock computation | nock.c |
u3nc |
u3q_ |
Jets (retain semantics, C arguments) | [a-g]/*.c |
|
u3r_ |
Retrieval; returns on error | retrieve.c |
u3r_word |
u3t_ |
Profiling and tracing | trace.c |
u3t |
u3v_ |
Arvo operations | vortex.c |
u3v_reclaim |
u3w_ |
Jets (retain semantics, Nock core argument) | [a-g]/*.c |
|
u3x_ |
Retrieval; crashes on error | xtract.c |
u3x_cell |
u3z_ |
Memoize | zave.c |
u3z_uniq |
The u3
system allows you to extract Urbit nouns as atoms or cells.
Atoms may come in one of two forms: either they fit in 31 bits or less
of a 32-bit unsigned integer, or they require more space. In the former
case, you will use the singular functions such as u3r_word
and
u3a_word
to extract and store information. If the atom is larger than
this, however, you need to treat it a bit more like a C array, using the
plural functions u3r_words
and u3a_words
. (For native sizes larger
than 32 bits, such as double-precision floating-point numbers, replace
word
with chub
in these.)
An audit of the jet source code shows that the most commonly used u3
functions include:
-
u3a_free
frees memory allocated on the loom (Vere memory model). -
u3a_malloc
allocates memory on the loom (Vere memory model). (Never use regular Cmalloc
inu3
.) -
u3i_bytes
writes an array of bytes into an atom. -
u3i_chub
is the ≥32-bit equivalent ofu3i_word
. -
u3i_chubs
is the ≥32-bit equivalent ofu3i_words
. -
u3i_word
writes a single 31-bit or smaller atom. -
u3i_words
writes an array of 31-bit or smaller atoms. -
u3m_bail
produces an error and crashes the process. -
u3m_p
prints a message and au3
noun. -
u3r_at
retrieves data values stored at locations in the sample. -
u3r_byte
retrieves a byte from within an atom. -
u3r_bytes
retrieves multiple bytes from within an atom. -
u3r_cell
produces a cell[a b]
. -
u3r_chub
is the >32-bit equivalent ofu3r_word
. -
u3r_chubs
is the >32-bit equivalent ofu3r_words
. -
u3r_mean
deconstructs a noun by axis address. -
u3r_met
reports the total size of an atom. -
u3r_trel
factors a noun into a three-element cell[a b c]
. -
u3r_word
retrieves a value from an atom as a Cuint32_t
. -
u3r_words
is the multi-element (array) retriever likeu3r_word
.
Defining jets which have a different sample size requires querying the correct nodes of the sample as binary tree:
1. 1 argument → `u3x_sam`
2. 2 arguments → `u3x_sam_2`, `u3x_sam_3`
3. 3 arguments → `u3x_sam_2`, `u3x_sam_6`, `u3x_sam_7`
4. 4 arguments → `u3x_sam_2`, `u3x_sam_6`, `u3x_sam_14`, `u3x_sam_15`
5. 5 arguments → `u3x_sam_2`, `u3x_sam_6`, `u3x_sam_14`, `u3x_sam_30`,
`u3x_sam_31`
6. 6 arguments → `u3x_sam_2`, `u3x_sam_6`, `u3x_sam_14`, `u3x_sam_30`,
`u3x_sam_62`, `u3x_sam_63`
A more complex argument structure requires grabbing other entries; e.g.,
|= [u=@lms [ia=@ud ib=@ud] [ja=@ud jb=@ud]]
requires
u3x_sam_2, u3x_sam_12, u3x_sam_13, u3x_sam_14, u3x_sam_15
Let us examine jet composition using a more complicated floating-point operation. The Urbit runtime uses SoftFloat to provide a reference software implementation of floating-point mathematics. This is slower than hardware FP but more portable.
This library lib/trig-rs.hoon
provides a few transcendental functions useful in many mathematical calculations. The ~%
"sigcen" rune registers the jets (with explicit arguments, necessary at the highest level of inclusion). The ~/
"sigfas" rune indicates which arms will be jetted.
:: Transcendental functions library, compatible with @rs
::
=/ tau .6.28318530717
=/ pi .3.14159265358
=/ e .2.718281828
=/ rtol .1e-5
~% %trig ..part ~
|%
:: Factorial, $x!$
::
++ factorial
~/ %factorial
|= x=@rs ^- @rs
=/ t=@rs .1
|- ^- @rs
?: =(x .0) t
?: =(x .1) t
$(x (sub:rs x .1), t (mul:rs t x))
:: Absolute value, $|x|$
::
++ absolute
|= x=@rs ^- @rs
?: (gth:rs x .0)
x
(sub:rs .0 x)
:: Exponential function, $\exp(x)$
::
++ exp
~/ %exp
|= x=@rs ^- @rs
=/ rtol .1e-5
=/ p .1
=/ po .-1
=/ i .1
|- ^- @rs
?: (lth:rs (absolute (sub:rs po p)) rtol)
p
$(i (add:rs i .1), p (add:rs p (div:rs (pow-n x i) (factorial i))), po p)
:: Integer power, $x^n$
::
++ pow-n
~/ %pow-n
|= [x=@rs n=@rs] ^- @rs
?: =(n .0) .1
=/ p x
|- ^- @rs
?: (lth:rs n .2)
p
::~& [n p]
$(n (sub:rs n .1), p (mul:rs p x))
--
We will create a generator which will pull the arms and slam each gate such that we can assess the library's behavior. Later on we will create unit tests to validate the behavior of both the unjetted and jetted code.
/+ *trig-rs
!:
:- %say
|= [[* eny=@uv *] [x=@rs n=@rs ~] ~]
::
~& (factorial n)
~& (absolute x)
~& (exp x)
~& (pow-n x n)
[%verb ~]
We will further define a few unit tests as checks on arm behavior:
/+ *test, *trig-rs
::
::::
::
|%
++ test-factorial ^- tang
;: weld
%+ expect-eq
!> .1
!> (factorial .0)
%+ expect-eq
!> .1
!> (factorial .1)
%+ expect-eq
!> .120
!> (factorial .5)
%+ expect-eq
!> .720
!> (factorial .6)
==
--
As before, the jet hints must provide a trail of symbols for the interpreter to know how to match the Hoon arms to the corresponding C code.
~% %trig-rs ..part ~
|%
++ factorial
~/ %factorial
|= x=@rs ^- @rs
...
++ exp
~/ %exp
|= x=@rs ^- @rs
...
++ pow-n
~/ %pow-n
|= [x=@rs n=@rs] ^- @rs
...
--
As before:
-
Register the jet symbols and function names in
tree.c
. -
Declare function prototypes in headers
q.h
andw.h
. -
Produce functions for compilation and linking in the
pkg/urbit/jets/e
directory.
Register the jet symbols and function names. A jet registration may
be carried out at in point in tree.c
. The registration consists of
marking the core
/* Jet registration of ++factorial arm under trig-rs */
static u3j_harm _140_hex__trigrs_factorial_a[] = {{".2", u3we_trigrs_factorial, c3y}, {}};
/* Associated hash */
static c3_c* _140_hex__trigrs_factorial_ha[] = {
"903dbafb8e59427eced0b35379ad617c2eb6083a235075e9cdd9dd80e732efa4",
0
};
static u3j_core _140_hex__trigrs_d[] =
{ { "factorial", 7, _140_hex__trigrs_factorial_a, 0, _140_hex__trigrs_factorial_ha },
{}
};
static c3_c* _140_hex__trigrs_ha[] = {
"0bac9c3c43634bb86f6721bbcc444f69c83395f204ff69d3175f3821b1f679ba",
0
};
/* Core registration by token for trigrs */
static u3j_core _140_hex_d[] =
{ /* ... pre-existing jet registrations ... */
{ "trig-rs", 31, 0, _140_hex__trigrs_d, _140_hex__trigrs_ha },
{}
};
Declare function prototypes in headers.
We must declare u3we_trigrs_factorial
and u3qe_trigrs_factorial
:
In w.h
:
u3_noun u3we_trigrs_factorial(u3_noun);
In q.h
:
u3_noun u3qe_trigrs_factorial(u3_atom);
Produce functions for compilation and linking.
Given these function prototype declarations, all that remains is the
actual definition of the function. Both functions will live in their own
file; we find it the best convention to associate all arms of a core in
a single file. In this case, create a file pkg/urbit/jets/e/trig-rs.c
and
define all of your trig-rs
jets therein. (Here we show ++factorial
only.)
/* jets/e/trig-rs.c
**
*/
#include "all.h"
#include <softfloat.h> // necessary for working with software-defined floats
#include <stdio.h> // helpful for debugging, removable after development
#include <math.h> // provides library fabs() and ceil()
union sing {
float32_t s; //struct containing v, uint_32
c3_w c; //uint_32
float b; //float_32, compiler-native, useful for debugging printfs
};
/* ancillary functions
*/
bool isclose(float a,
float b)
{
float atol = 1e-6;
return ((float)fabs(a - b) <= atol);
}
/* factorial of @rs single-precision floating-point value
*/
u3_noun
u3qe_trigrs_factorial(u3_atom u) /* @rs */
{
fprintf(stderr, "u3qe_trigrs_factorial\n\r"); // DELETE THIS LINE LATER
union sing a, b, c, e;
u3_atom bb;
a.c = u3r_word(0, u); // extricate value from atom as 32-bit word
if (ceil(a.b) != a.b) {
// raise an error if the float has a nonzero fractional part
return u3m_bail(c3__exit);
}
if (isclose(a.b, 0.0)) {
a.b = (float)1.0;
return u3i_words(1, &a.c);
}
else if (isclose(a.b, 1.0)) {
a.b = (float)1.0;
return u3i_words(1, &a.c);
}
else {
// naive recursive algorithm
b.b = a.b - 1.0;
bb = u3i_words(1, &b.c);
c.c = u3r_word(0, u3qe_trig_factorial(bb));
e.s = f32_mul(a.s, c.s);
u3m_p("result", u3i_words(1, &e.c)); // DELETE THIS LINE LATER
return u3i_words(1, &e.c);
}
}
u3_noun
u3we_trigrs_factorial(u3_noun cor)
{
fprintf(stderr, "u3we_trigrs_factorial\n\r"); // DELETE THIS LINE LATER
u3_noun a;
if ( c3n == u3r_mean(cor, u3x_sam, &a, 0) ||
c3n == u3ud(a) )
{
return u3m_bail(c3__exit);
}
else {
return u3qe_trigrs_factorial(a);
}
}
This code deviates from the integer implementation in two ways: because all @rs
atoms are guaranteed to be 32-bits, we can assume that c3_w
can always contain them; and we are using software-defined floating-point operations with SoftFloat.
We have made use of u3r_word
to convert a
32-bit (really, 31-bit or smaller) Hoon atom (@ud
) into a C uint32_t
or c3_w
. This unsigned integer may be interpreted as a floating-point
value (similar to a cast to @rs
) by the expedient of a C union
,
which allows multiple interpretations of the same bit pattern of data;
in this case, as an unsigned integer, as a SoftFloat struct
, and as a
C single-precision float
.
f32_mul
and its sisters (f32_add
, f64_mul
, f128_div
, etc.) are
floating-point operations defined in software. These are not as
efficient as native hardware operations would be, but allow Urbit to
guarantee cross-platform compatibility of operations and not rely on
hardware-specific implementations. Currently all Urbit floating-point
operations involving @r
values use SoftFloat.
With this one jet for ++factorial
in place, compile the jet and take
note of where Nix produces the binary.
make
Copy the affected files back into the ship's pier:
cp home/lib/trig-rs.hoon zod/home/lib
cp home/gen/trig-rs.hoon zod/home/gen
Restart your fakezod using the new Urbit binary and synchronize these to
the %home
desk:
|commit %home
If all has gone well to this point, you are prepared to test the jet
using the %say
generator from earlier:
+trig 5
Among the other output values, you should observe the stderr
messages
emitted by the jet functions each time they are called.
- The type union remains necessary to easily convert the floating-point result back into an unsigned integer atom.
/* integer power of @rs single-precision floating-point value
*/
u3_noun
u3qe_trigrs_pow_n(u3_atom x, /* @rs */
u3_atom n) /* @rs */
{
fprintf(stderr, "u3qe_trig_pow_n\n\r");
union sing x_, n_, f_;
x_.c = u3r_word(0, x); // extricate value from atom as 32-bit word
n_.c = u3r_word(0, n);
f_.b = (float)pow(x_, n_);
return u3i_words(1, &f_.c);
}
u3_noun
u3w_trigrs_pow_n(u3_noun cor)
{
fprintf(stderr, "u3w_trig_pow_n\n\r");
u3_noun a, b;
if ( c3n == u3r_mean(cor, u3x_sam_2, &a,
u3x_sam_3, &b, 0) ||
c3n == u3ud(a) || c3n == u3ud(b) )
{
return u3m_bail(c3__exit);
}
else {
return u3q_trigrs_pow_n(a, b);
}
}
We leave the implementation of the other jets to the reader as an exercise. (Please do not skip this: the exercise will both solidify your understanding and raise new important situational questions.)
Again, the C jet code need not follow the same logic as the Hoon source
code; in this case, we simply use the built-in math.h
pow
function. (We could—arguably should—have used SoftFloat
functions.)
A pill is a Nock "binary blob", really a parsed Hoon abstract syntax
tree. Pills are used to bypass the bootstrapping procedure for a new
ship, and are particularly helpful when jetting code in hoon.hoon
,
%zuse
, %lull
, or the main Arvo vanes.
The legacy jetting tutorial contains specific instructions on how to compile a pill and work with a more rapid setup for jetting with fakezods.
You don't strictly need to use pills in producing jets, but it can speed up your development cycle.
All nontrivial code should be thoroughly tested to ensure software quality. To rigorously verify the jet's behavior and performance, we will combine live testing in a single Urbit session, comparative behavior between a reference Urbit binary and our modified binary, and unit testing.
-
Live spot checks rely on you modifying the generator
trig-rs.hoon
and observing whether the jet works as expected.When producing a library, one may use the
-build-file
thread to build and load a library core through a face. Two fakezods can be operated side-by-side in order to verify consistency between the Hoon and C code.> =trig-rs -build-file %/lib/trig-rs/hoon > (exp:trig-rs .5)
-
Comparison to the reference Urbit binary can be done with a second fakezod and the same Hoon library and generator.
-
Unit tests rely on using the
-test
thread and will be covered subsequently.> -test %/tests/lib/trig-rs ~
We omit from the current discussion a few salient points:
-
Reference counting with transfer and retain semantics. (For everything the new developer does outside of real Hoon shovel work, one will use transfer semantics.)
-
The structure of memory: the loom, with outer and inner roads.
-
Many details of C-side atom declaration and manipulation from the
u3
library.
We commend to the reader the exercise of selecting particular
Hoon-language library functions provided with the system, such as
++cut
, locating the corresponding jet code
and learning in detail how
particular operations are realized in u3
C. Note in particular that
jets do not need to follow the same solution algorithm and logic as the
Hoon code; they merely need to reliably produce the same result. At the
current time, the feasibility of automatic jet verification is an open
research question.