Reference
Every command and flag of both tools, and the Canon format in brief. The full format is in spec/CANON.md.
bxm-canon
bxm-canon encode [FILE] [-o OUT] Canon text (JSON) in, canonical bytes out
bxm-canon decode [FILE] [--pretty] canonical bytes in, Canon text out
bxm-canon fmt [FILE] [--pretty] text in, the one normal text form out
bxm-canon check FILE... exit 0 only if every file is canonical bytes
bxm-canon hash [FILE] [--text] SHA-256 of the canonical bytes
bxm-canon diff A B [--text] paths that differ between two values
bxm-canon version
FILE defaults to standard input, and - also means standard input. --text tells hash and diff that the inputs are Canon text rather than canonical bytes. -o writes to a file instead of standard output.
Exit codes: 0 when all is well, 1 when check finds a file that isn’t canonical or diff finds a difference, 2 for any other error.
Diff paths
diff prints one line per difference: + for a path only in the second value, - for a path only in the first, ~ for a value that changed. Paths start at $, with .key for map keys, ["a key"] for keys that aren’t plain identifiers and [3] for array positions.
~ $.version 3 -> 4
- $.tags[1] "b"
+ $["release date"] "2026-10-22"
Canon Format
| Tag | Type | After the tag |
|---|---|---|
00 |
null | nothing |
01 |
false | nothing |
02 |
true | nothing |
03 |
integer | zigzag value as a varint |
04 |
float | 8 IEEE 754 bytes, big-endian |
05 |
bytes | length, then the bytes |
06 |
string | length, then UTF-8 |
07 |
array | count, then each item |
08 |
map | count, then key and value pairs, keys in byte order |
Lengths, counts and integers are unsigned LEB128 varints in their shortest form. The decoder rejects:
| Error code | Meaning |
|---|---|
unexpected-end |
the input ends inside a value |
unknown-tag |
a tag byte outside 00 to 08 |
overlong-varint |
a varint longer than it needs to be |
varint-too-large |
a varint that doesn’t fit in 64 bits |
nan, infinity, negative-zero |
floats that aren’t Canon values |
invalid-utf8 |
a string or key that isn’t valid UTF-8 |
length-too-large |
a length or count larger than the input left |
keys-out-of-order |
map keys not in byte order |
duplicate-key |
the same map key twice |
too-deep |
arrays and maps nested deeper than 512 |
trailing-bytes |
anything after the one top-level value |
Canon text
Canon text is JSON with three rules. A number without ., e or E is a 64-bit integer, and one too large is an error rather than a float. A repeated key is an error. Bytes are written h"0aff", with lowercase hex.
C API
int bxm_canon_from_text(const uint8_t *text, size_t len, uint8_t **out, size_t *out_len, char **err);
int bxm_canon_to_text(const uint8_t *data, size_t len, uint8_t **out, size_t *out_len, char **err);
int bxm_canon_check(const uint8_t *data, size_t len, char **err);
int bxm_canon_hash(const uint8_t *data, size_t len, char hex_out[65], char **err);
void bxm_canon_free(uint8_t *p, size_t len);
void bxm_canon_free_str(char *err);
const char *bxm_canon_version(void);
Each function returns 0 on success. Buffers it hands out are released with bxm_canon_free, messages with bxm_canon_free_str. Pass NULL for err to skip messages.
JavaScript
import { load, CanonError } from "bxm-canon";
const canon = await load();
| Function | What it does |
|---|---|
fromText(text) |
Canon text to canonical bytes (Uint8Array) |
toText(bytes, { pretty }) |
canonical bytes to text; throws if they aren’t canonical |
fmt(text, { pretty }) |
text to its one normal form |
check(bytes) |
throws a CanonError unless the bytes are one canonical value |
isCanonical(bytes) |
the same check as true or false |
hash(bytes) |
SHA-256 hex of canonical bytes |
hashText(text) |
SHA-256 hex of the canonical encoding of a text value |
diffText(a, b) |
the differences between two values, one line each |
version |
version of the Canon core |
A CanonError carries code, one of the error codes above or text, and the byte offset. Strings with an unpaired surrogate are refused. To hash a JavaScript object, pass JSON.stringify(obj) to hashText, keeping in mind that JavaScript writes 1.0 as 1, so whole numbers become integers.
Python
import bxm_canon as canon
from bxm_canon import CanonError
| Function | What it does |
|---|---|
encode(obj) |
Python value to canonical bytes |
decode(data) |
canonical bytes to a Python value; raises CanonError if not canonical |
check(data) |
raises CanonError unless the bytes are one canonical value |
is_canonical(data) |
the same check as True or False |
hash(data) |
SHA-256 hex of canonical bytes |
hash_value(obj) |
SHA-256 hex of a Python value’s canonical encoding |
from_text(text) |
Canon text to canonical bytes |
to_text(data, pretty=False) |
canonical bytes to text |
fmt(text, pretty=False) |
text to its one normal form |
hash_text(text) |
SHA-256 hex of a text value’s canonical encoding |
diff_text(a, b) |
the differences between two text values, one line each |
Python values map one to one: None, bool, int (64-bit), float, str, bytes (and bytearray or a byte memoryview), list and tuple (decoded as list), and dict with str keys. A larger int raises OverflowError; NaN, infinity, -0.0 and lone surrogates raise CanonError; other types raise TypeError. CanonError is a ValueError with code and offset, and it pickles.
bxm-memo
bxm-memo run [flags] -- COMMAND [ARGS...] restore outputs on a hit, run on a miss
bxm-memo explain [flags] -- COMMAND [ARGS...] say hit or miss and why; runs nothing
bxm-memo list cached results, newest first
bxm-memo gc [--older-than 30d] [--max-size 2G] remove old results and unused files
bxm-memo clear remove the whole cache
bxm-memo version
Flags for run and explain
| Flag | Meaning |
|---|---|
-i, --in PATH |
An input: a file, a directory (every file under it), or a pattern with *, ?, [...] and ** for any number of directories. Repeat for more. |
-o, --out PATH |
An output file or directory the command writes. At least one is needed. Repeat for more. |
-e, --env NAME |
An environment variable whose value is part of the key. Repeat for more. |
-n, --name NAME |
A name for the job, used to explain misses against its last run. |
--salt TEXT |
Extra text in the key, to throw away old results on purpose. |
--cache DIR |
The cache directory. |
-q, --quiet |
No hit or miss lines on standard error. |
Everything after -- is the command. Quote patterns so the shell doesn’t expand them first: -i 'src/**/*.go'.
The cache
The cache directory is --cache, or $BXM_MEMO_CACHE, or bxm-memo in the user cache directory (~/.cache on Linux, ~/Library/Caches on macOS, %LocalAppData% on Windows). It holds an entry per result, the last run of each job for explaining misses, and the stored files named by their SHA-256.
gc with no flags removes results older than 30 days. With --max-size it keeps the newest results that fit. Files younger than an hour are never removed, because another run may be storing them.
Exit codes
On run, the command’s own exit code. On a hit, 0. When Memo itself fails, for example on a bad flag or an input pattern that matches nothing, 125. explain exits 0 for a hit and 1 for a miss.
What goes into the key
The command line, the contents and executable bit of every input file and its path, the values of the declared environment variables, the declared output paths, the operating system and CPU type, the salt, and Memo’s cache format version.