A faithful Malbolge interpreter (Python) and a verified cat program, plus a
heavy differential test suite that diffs the interpreter against Ben Olmstead's
original 1998 reference C interpreter.
| File | What it is |
|---|---|
malbolge.py |
The interpreter. CLI: python3 malbolge.py prog.mb [--max-steps N]. |
cat.mb |
A Malbolge cat: copies stdin to stdout, halts on EOF. |
hello.mb |
Prints Hello World!. |
text2malbolge.py |
Linear text → Malbolge compiler (reference-safe; fast; ASCII scope). |
text2malbolge_bor.py |
Branch-on-read compiler: full arbitrary-byte output (all 256 values). |
tests/malbolge_dbg.c |
A from-scratch guarded+capped C interpreter, used as a fuzzing oracle. |
tests/diff_test.py |
Interpreter differential + fuzz + edge-case + CLI suite. |
tests/gen_test.py |
Linear compiler verification (programs run on the reference C). |
tests/bor_test.py |
Branch-on-read verification: all 256 bytes on the reference C. |
printf 'Meow!' | python3 malbolge.py cat.mb # -> Meow!
python3 malbolge.py hello.mb # -> Hello World!
python3 text2malbolge.py "Hi Dmitry" > p.mb && \
python3 malbolge.py p.mb # -> Hi DmitryI/O is lazy and streaming: a program is never blocked waiting for input it
does not read (/), and output appears as it is produced, exactly like the
reference's getc/putc.
malbolge.py is a line-for-line reimplementation of Olmstead's malbolge.c:
- 59049 = 3¹⁰ words; registers
a,c,dstart at 0. - Instruction decode:
xlat1[(mem[c] - 33 + c) % 94], dispatched overj i * p < / v o. - The
crazyop is the verbatim base-9o[9][9]table from the referenceop()(not a hand-derived 3×3), so there is no transcription risk. - Rotate, the
xlat1/xlat2tables, memory fillop(mem[i-1], mem[i-2]), EOF →a = 59048, and outputa & 0xFFall match. - Loader: skips C-
isspacewhitespace; rejects a graphical char that does not decode to one ofji*p</vo; accepts non-graphical, non-whitespace bytes (stored as-is); rejects programs longer than 59049 instructions.
The reference relies on C undefined behaviour in exactly two spots; we take the well-defined interpretation that every modern interpreter and the esolang spec agree on:
-
Out-of-bounds encryption. The reference does
mem[c] = xlat2[mem[c]-33]unconditionally. Whenc == dand a*/presult leaves the cell outside 33..126, that reads past the 94-bytexlat2array (and crashes the real interpreter — see note below). We apply encryption only when the cell is graphical. The over-written cell is never re-read in that situation, so observable output is identical for any program where the reference does not crash. -
Non-instruction cell. The reference's
if (mem[c]<33||mem[c]>126) continue;skips the increment, so the machine spins forever with no further effect. We detect this provably-terminal state and return the output produced so far — observably identical to the hung reference.
Note: a naive linear generator that keeps
d == cproduces programs that trip exactly this case and crash the unmodified reference (out-of-bounds read).text2malbolge.pyavoids it (see below);cat.mb/hello.mbkeepd != cand run on the real reference interpreter unchanged.
text2malbolge.py emits programs that keep d = c - 1: the operand of
every rotate/crazy is the previous, already-encrypted cell (always graphical),
and results are written behind the code pointer, so the current code cell
stays graphical and the reference's encryption never goes out of bounds. The
generated programs therefore run on Olmstead's unmodified interpreter.
Each output byte is reached by a breadth-first search over {nop, rotate, crazy} against the forced operand stream. The linear technique can reach about
201 of 256 byte values from the initial state -- enough for all printable
ASCII (the common case). The unreachable high bytes are a property of the
technique, not this implementation: the original zb3 linear generator loops
forever on them, whereas text2malbolge.py raises Unreachable immediately.
text2malbolge_bor.py removes the reachability limit by reading the operand
from a controlled data region the code never executes, so the operand of
every rotate/crazy is a byte we choose — and the accumulator can be driven to
any value, so every byte 0..255 is emittable (verified on the reference C).
python3 text2malbolge_bor.py "café 🎉" > p.mb && python3 malbolge.py p.mbIt branches on the value of each data cell read, but over a fixed code/data split instead of a blind whole-program search (which is exponential — even zb3's blind BoR can't emit the bytes in ~154..208 in reasonable time). The layout, all verified by tracing the real machine:
pos 0:movd→ after the cyclec=1, d=41.pos 1:movdreadsmem[41](locked to the nop-char 121) →c=2, d=122, i.e.d = c + 120from here on: the data pointer runs 120 cells ahead of the code pointer.pos 2..121: code (rotate / crazy / out / halt); position 41 is reserved.pos 122..: data. Code at positionpreadsmem[p+120].
Because the data pointer is 120 cells ahead, the cells it reads are not yet
executed or encrypted, so we may store any byte there. We use the 155
non-whitespace, non-graphical byte values (1..8, 14..31, 127..255), which the
loader stores verbatim with no instruction validation. Rotate/crazy write their
result onto the data cell (d != c), so the executing code cell stays graphical
and the reference's encryption never goes out of bounds.
Each output byte is reached by a tiny BFS over the accumulator (transitions
a=rotr(V) and a=crazy(a,V)), usually 1–2 ops; the whole 0..255 sweep
compiles in ~6 s (~24 ms/byte). Capacity is ~40–50 output bytes (120 code
cells); past that it raises TooLong — use text2malbolge.py for longer ASCII
text.
python3 tests/diff_test.py (needs the reference built at /tmp/malbolge_ref
and tests/malbolge_dbg compiled):
- Deliverables vs the unmodified Olmstead reference C:
hello.mb→Hello World!on both.cat.mb→ 826 inputs diffed byte-for-byte (empty, every single byte, every doubled byte, full + reversed 0–255, 300 random binary blobs): 826/826 exact matches.
- Fuzzing vs the guarded C oracle: 10,000 random raw-byte programs + 10,000 random valid-op programs (random input, random step caps): 20,000/20,000 agreed on accept/reject and output.
- Reference cross-check: 400 random valid programs run on the unmodified reference; all that halted there matched.
- Spec edge cases: rotate/crazy values, empty program, whitespace-only,
single
v, invalid-instruction rejection, non-printable acceptance, the 59049 length cap — all pass. - CLI:
hello.mbwith stdin closed must not block;cat.mbpiped/binary echo — all pass.
Total: ~21k checks, 0 failures.
python3 tests/gen_test.py (the linear compiler):
- 228 ASCII texts compiled; every generated program reproduces its text on both the unmodified reference C and our interpreter.
- Known-unreachable bytes raise
Unreachablein <0.1s (no hang).
Total: 232 checks, 0 failures.
python3 tests/bor_test.py (the branch-on-read compiler):
- All 256 byte values compiled and run on the unmodified reference C — 256/256 exact (~6 s total).
- 17 multi-byte messages (UTF-8 café/Москва/日本語, high-byte runs, NULs, random
up to 40 bytes), empty/NUL/0xFF edges, and clean
TooLongpast capacity.
Total: 277 checks, 0 failures.