An AT Protocol / Bluesky client where every string value in every server response is computed by Malbolge — a programming language deliberately designed to be impossible to use, named after the eighth circle of hell in Dante's Inferno.
Not echoed. Not passed through. Computed.
The handle, the DID, the post URI — each one is the output of one or more purpose-built Malbolge programs generated on the fly, run through a Malbolge virtual machine, and assembled by an Ada bridge before being returned to the caller.
Malbolge was created in 1998 by Ben Olmstead. The stated goal was explicit: make a language that is as difficult to program in as possible. It achieved this through several mechanisms working in concert:
Ternary memory. The VM operates in base 3 (ternary), not binary. All values are 10-trit numbers in the range 0–59048.
Self-modifying code. Every instruction modifies itself after execution via a lookup table (xlat2). The program you wrote is not the program that runs. By the second pass through any instruction, it has already been replaced.
The crazy operation (crz). The primary arithmetic operation takes two 10-trit numbers and applies a lookup table trit by trit. There is no intuitive analogue in normal computing. crz(0, 81) = 29443. crz(29443, 56) = 29552. 29552 % 256 = 112 = 'p'. That is how you output the letter p.
Instruction encoding. Valid source characters must be printable ASCII (33–126). The VM validates this on load and rejects anything outside that range.
The author never wrote a working program in it. The first working Malbolge program — a hello world — took two years to appear after the language was published, and it was not written by hand. Andrew Cooke generated it using a beam search algorithm.
When you call any operation, every string value in the response is computed by Malbolge:
hell: generating for [at://did:plc:qntugqppm75ya4n73muu3qhp/app.bsky.feed.post/3mibrgb3piw2c]
hell: chunk [chunk_00.mb] -> [a]
hell: chunk [chunk_01.mb] -> [t://did]
hell: chunk [chunk_02.mb] -> [:]
hell: chunk [chunk_03.mb] -> [plc]
hell: chunk [chunk_04.mb] -> [:]
hell: chunk [chunk_05.mb] -> [qntugqppm7]
hell: chunk [chunk_06.mb] -> [5ya4n73muu]
hell: chunk [chunk_07.mb] -> [3qhp/]
hell: chunk [chunk_08.mb] -> [a]
hell: chunk [chunk_09.mb] -> [pp.bsky.fe]
hell: chunk [chunk_10.mb] -> [ed.post/3m]
hell: chunk [chunk_11.mb] -> [ibrgb3piw2]
hell: chunk [chunk_12.mb] -> [c]
hell: assembled [at://did:plc:qntugqppm75ya4n73muu3qhp/app.bsky.feed.post/3mibrgb3piw2c]
13 Malbolge programs for one URI. That post is live on Bluesky.
The : characters in did:plc: each require their own program — they are harder to reach through crz arithmetic and need a different instruction sequence. Everything else splits into 10-character chunks.
There is. malbolge/ping_pong.mb is a 300-byte Malbolge program that outputs pong\n and halts cleanly. You can run it:
./malbolge/malbolge_vm ./malbolge/ping_pong.mbThe other programs are generated on the fly by generate_malbolge.py for each string value that needs to pass through hell. They are written to /tmp/malbolgesky/ and run immediately. They are valid Malbolge programs — all bytes printable ASCII 33–126, accepted by the VM, producing correct output.
The reason they are not checked in is that they are different for every string. The handle formerlab.bsky.social produces different programs than alice.bsky.social. The generator is the artifact. The programs are its output.
This is the hard part. It took most of a session to get right.
To output a character with ASCII value T, you need the Malbolge accumulator register to satisfy a % 256 == T when a < (output) instruction executes.
The only way to set the accumulator to an arbitrary value is through the crz operation. Starting from a = 0, the generator searches for values d1 and d2 (both in printable ASCII range 33–126, since they must live in the program) such that:
crz(crz(0, d1), d2) % 256 == T
This is a 2-chain. For most characters this works. Some characters — notably : (58) from certain accumulator states — require a 3-chain: three crz operations in sequence.
The accumulator value after outputting each character becomes the starting value for the next character's chain. The chains are stateful — formerlab.bsky.social is not 21 independent problems, it is one problem where each solution constrains the next.
For a 2-chain character, the instruction sequence is:
j, *, j, *, <
jredirects the data pointer via memory indirection*performs crz using the data pointer's target as operand<outputsa % 256
For a 3-chain character:
j, *, j, *, j, *, <
Getting this right took hours. The constraints:
- All program bytes must be in printable ASCII range 33–126
- The data pointer
dand instruction pointercboth start at 0 and both auto-increment after every step, keeping them in sync unless redirected byj - The
*instruction writes its result back to the address it read from, clobbering the data — subsequent operations cannot reuse the same address - Pointer table entries must themselves be in 33–126 (constraining the address space to 94 positions)
- Using consecutive pointer positions causes
D_nextaliasing — the position where the next character's chain starts collides with the pointer table of the previous character
The fix for aliasing: sparse stride-4 pointer tables. PTR2 positions are spaced 4 apart (PTR2[i] = PTR2_BASE + i*4). The D_next value PTR2[i] + 2 then equals PTR2[i + 0.5], which is never an integer — no alias possible.
PLEASE NOTE is INTERCAL. In Malbolge, there is no comment syntax. Every byte is potentially an instruction, and the self-modifying xlat2 table determines what each byte does after it has executed once.
The challenge is that program bytes at positions 33–126 must not accidentally execute as wrong instructions at the wrong time. The generator assigns instruction bytes (j=106, *=42, <=60, v=118) to specific positions and fills data regions with values chosen to be NOPs in the Malbolge execution context.
The address space (33–126, 94 positions) limits how many characters fit in one program. The NAV region starts at position 107 (because j=106 is the first instruction and after execution d lands at 107). For N=10 characters, NAV uses 107–126 — exactly filling the space.
Strings longer than 10 characters are split into chunks. Strings containing 3-chain characters get those characters split into separate homogeneous chunks (the 2-chain and 3-chain instruction patterns cannot be mixed in one program without complex layout changes).
malbolge/malbolge_cat.mb is a 4096-byte Malbolge program consisting entirely of /< repeated 2048 times. / reads a character from stdin into the accumulator. < writes the accumulator to stdout. This is Malbolge as a transparent transport — every JSON request passes through the VM and emerges unchanged.
The Malbolge self-modifying table means a naive /</</< loop would not work — instructions encrypt themselves after execution. The unrolled approach (/< × 2048) sidesteps this entirely. It reads at most 2048 characters and halts cleanly.
Your JSON line
→ Ada: parse request
→ Malbolge cat program: echo transport (every byte through the VM)
→ Ada: call bsky.social XRPC via libcurl
→ Ada: extract string values from response
→ generate_malbolge.py: compute crz chains, write chunk programs
→ Malbolge VM: run each chunk, output string fragment
→ Ada: concatenate chunk outputs
→ Ada: return JSON with Malbolge-computed values
gprbuildand GNAT (Ada compiler)libcurl-dev- Python 3 (for
generate_malbolge.py) - gcc (to build the Malbolge VM)
# Build the Malbolge VM
gcc -O2 malbolge/malbolge.c -o malbolge/malbolge_vm
# Build the Ada bridge
cd ada
gprbuild -P adatalksky_core.gpr -fcd /path/to/malbolgesky
printf '{"op":"ping","params":{}}\n' | ./ada/bin/mainExpected response:
{"ok":true,"result":"pong"}With stderr showing:
hell: ping_pong.mb -> [pong]
Login and post:
printf '{"op":"login","params":{"identifier":"you.bsky.social","password":"your-password"}}\n' | ./ada/bin/main
printf '{"op":"createPost","params":{"text":"Hello from Malbolge. Every string in this response was computed by the eighth circle of hell."}}\n' | ./ada/bin/main| File | Description |
|---|---|
malbolge/malbolge.c |
Malbolge VM — Ben Olmstead 1998, public domain |
malbolge/malbolge_cat.mb |
4096-byte cat program (/< × 2048) |
malbolge/ping_pong.mb |
300-byte program that outputs pong\n |
generate_malbolge.py |
Generates Malbolge programs for arbitrary strings |
ada/src/hell_transport.adb |
Ada layer that routes strings through the VM |
ada/src/bridge_protocol.adb |
Protocol routing and JSON handling |
- Fortransky — Fortran
- Assemblersky — x86 Assembly
- Cobolsky — COBOL [private repo - release soon]
- Adatalksky — Ada + Pharo Smalltalk [private repo - release soon]
- Malbolgesky — Malbolge ← you are here
- INTERCALsky — INTERCAL
Each one is a proof that the AT Protocol works regardless of what's on the other end.
The eighth circle of hell is in the call stack.