Wota Format
Wota is the binary message format for intra-runtime transfer: letters
(actor mailboxes), boot records, lifecycle/log/trace records, signal events,
and call-gate payloads. It works at word granularity rather than byte
granularity — less compact than Nota but faster to arrange and consume. Wota
lives in the shoplib package: use('shoplib::wota').
Wota stands for Word Object Transfer Arrangement.
Value semantics and codec boundary
shoplib::wota owns value traversal. For each property or root it invokes
toJSON once on the original value, invokes the replacer once on that result,
and then traverses the replacement. It also owns cycle rejection, record and
array traversal, reviver order, and private actor-value policy.
The native codec accepts a flat preorder token stream and performs bounded word parsing, scalar conversion, and byte arrangement. Every native decode is given an explicit byte length and validates exactly one complete value before allocating heap values. Signal and call-gate inputs use the same validator. The call-gate response bridge walks containers in C without hooks because it runs after an arbitrary callback, where reentering Pit can suspend or reenter the VM; this is a bounded mailbox bridge rather than public codec semantics.
The cold-boot runtime temporarily exposes the same primitive module as
internal/wota_bootstrap so the first engine_lite can receive its initial
record before ordinary Pit modules run. The public locator remains
shoplib::wota.
Scope: one runtime, one width
Wota never crosses a machine boundary. The courier re-encodes envelopes to
Nota on the wire (both directions), the daemon CLI protocol is JSON
lines, and durable records stay Nota/JSON. Because every encode is decoded
by the same runtime build, the word width is a profile axis
(PIT_WOTA_BITS in source/pit_profile.h, an independent axis of the
profile — a recipe may pair a narrow value word with a wider wota when that
is what the target CPU handles fast) and folds into PIT_PROFILE_STRING —
artifacts that embed wota bytes (boot records; the exec records a cart
carries as start-plan data inside its nota metadata) are stamped with the
full profile and refuse to load under another width.
Width variants
One format family, three widths. Every value starts with a one-word preamble: the least significant byte is the type code, the remaining bits are the payload.
| Width | Word | Payload | Used by |
|---|---|---|---|
| wota64 | 64-bit | 56-bit | nan64 / f32 / fixed profiles (hosted dev) |
| wota32 | 32-bit | 24-bit | nan32 (playdate), tag32 when it lands |
| wota16 | 16-bit | 8-bit | val16 (future nano profile; codec groundwork in source/wota16.h) |
wota64 never spills: 56 bits hold every payload the runtime can produce. The narrow widths spill payloads that do not fit into a kim continuation immediately after the preamble word:
- Unsigned payloads (array/record counts, blob bit lengths, text byte
lengths, symbol codes): values below the all-ones payload (
0xffffffon wota32,0xffon wota16) encode directly. The all-ones payload is the spill sentinel: the true value follows as kim bytes. A value equal to the sentinel itself must therefore spill too. - Integers: the payload is a signed two’s-complement field read with an
arithmetic shift (as wota64). Every signed payload value except the
minimum encodes directly; the minimum bit pattern (
0x800000/0x80) marks a spill, and zigzag(value) ((n << 1) ^ (n >> 63)) follows as kim bytes. Spilled integers are exact over the full 64-bit range.
Kim bytes are the Nota varint: 7 value bits per byte, high bit set on every byte except the last, most significant group first. They pack into whole words like text bytes (first kim byte in the most significant byte of the first word) and the final word is zero-padded.
Blob bit lengths make the spill real on a 32-bit profile: 2^24 bits is
only 2 MiB, well inside a nan32 window. Counts that large cannot exist as
heap records, but the codec still encodes and decodes them correctly.
The generic codec lives in source/wota_codec.h; source/wota.h
instantiates it as the live wota_* API when PIT_WOTA_BITS == 32 and
keeps the historical 64-bit implementation otherwise.
Type Summary
| Byte | Type |
|---|---|
00 | Integer |
01 | Floating Point |
02 | Array |
03 | Record |
04 | Blob |
05 | Text |
07 | Symbol |
Preambles
Every Wota value starts with a preamble word. The least significant byte contains the type. The remaining bits (56 on wota64, 24 on wota32, 8 on wota16) contain type-specific data, spilling on the narrow widths as described above. Examples below show wota64 words unless noted.
Blob
A blob is a string of bits. The remaining field contains the number of
bits. The number of words that follow: ceil(number_of_bits / word_bits).
The first bit of the blob goes into the most significant bit of the first
word. The final word is padded with 0.
Example: A blob containing 25 bits 111100001110001100100001:
0000000000001904 # preamble: 25 bits, type blob
F0E3208000000000 # data (padded to 64 bits)
The same value as wota32 words: 00001904, F0E32080.
Text
The text is a string of UTF-8 bytes packed most-significant-first,
word_bits / 8 per word. The remaining field contains the number of
bytes. The number of words that follow:
ceil(number_of_bytes / (word_bits / 8)). The final word is padded with 0.
Example: "cat":
0000000000000305 # preamble: 3 UTF-8 bytes, type text
6361740000000000 # 'c' 'a' 't' + padding
The same value as wota32 words: 00000305, 63617400; as wota16 words:
0305, 6361, 7400.
Array
An array is an ordered sequence of values. The remaining field contains the number of elements. Following the preamble are the elements, each beginning with its own preamble. Nesting is encouraged. Cyclic structures are not allowed.
Example: ["ox", "cat"]:
0000000000000202 # preamble: 2 elements, type array
0000000000000205 # text "ox": 2 bytes
6F78000000000000 # 'o' 'x'
0000000000000305 # text "cat": 3 bytes
6361740000000000 # 'c' 'a' 't'
A wota32 array whose count reaches the sentinel spills:
FFFFFF02 # preamble: spill sentinel, type array
87FFFF7F # kim(16777215), one word
... # 16777215 elements follow
Record
A record is a set of key/value pairs. Keys must be text. The remaining field contains the number of pairs. Following the preamble the pairs are laid out as key, value, key, value, …
Example: {"ox": true}:
0000000000000103 # preamble: 1 pair, type record
0000000000000205 # key "ox": 2 bytes
6F78000000000000 # 'o' 'x'
0000000000000307 # value: symbol true
Number
An integer is carried in the preamble itself: the payload field holds the signed value (arithmetic shift up 8 bits to arrange, arithmetic shift down to consume). wota64 carries 56-bit integers this way; the runtime’s value encoder routes anything wider through the float form. On the narrow widths integers beyond the direct payload spill (see Width variants) and stay exact.
Example: 7:
0000000000000700 # integer 7
A wota32 integer that does not fit 24 signed bits, 8388608:
80000000 # preamble: int spill pattern
88808000 # kim(zigzag(8388608)) = kim(16777216)
A floating point number is the raw IEEE-754 double: the preamble word (payload zero) followed by the 8 raw bytes in native byte order — one word on wota64, two on wota32, four on wota16.
Example: 4.25:
0000000000000001 # preamble: type floating point
4011000000000000 # IEEE-754 bits of 4.25
Symbol
The remaining field contains the symbol.
Example: [null, false, true, private, system]:
0000000000000502 # array of 5
0000000000000007 # null
0000000000000207 # false
0000000000000307 # true
0000000000000807 # private
0000000000000907 # system
A private symbol is followed by one value (the actor-private payload);
decoders that skip values must skip the symbol’s trailing value too.