· 8 min read

The cabinet of impossible machines

Five languages from one archived esolang repository show what happens when functions, bits, sets, a self-modifying machine description, or Peano arithmetic is forced to carry an entire program.

A trompe-l'oeil painting of a display cabinet with open glass doors, its shelves crowded with curiosities, prints, and instruments.
Domenico Remps, Public domain

The most useful language-design repository I found this week had fourteen stars when I opened it, an archive banner, and no roadmap. Its package manifest names Thomas as the author, leaves the contributor list empty, and exposes eighty esoteric interpreters through one Node package. Somebody built real infrastructure for eighty toy languages and then, as far as I can tell, walked away.

I almost credited every language in the Hakerh400/esolangs archive to one person before noticing that it hosts other people's designs too. So I cross-checked against Hakerh400's Esolang author page and picked five that appear there and have interpreters in the archive, then read the specifications alongside the implementations instead of pretending an archived package is a maintained development kit.

What draws me to esolangs is that they can't cheat. A production language hides an awkward semantic choice under a library, a convention, an optimizer, a foreign-function call. These five have nowhere to put one. Each lets a single kind of thing carry almost the entire program, and then you find out what that costs.

MachineEverything isThe giftThe bill
Functional()Function callsClosures from a tiny coreNames acquire meaning by arrival order
Bitwise TranceMutable bitsBranching and self-modification coincideInstruction boundaries do not survive
ZFC++Finite setsExtensional equality is built inOrder and multiplicity must be encoded
Self-modifying Turing machineOne tapeThe machine description becomes dataA local write can rewrite the machine
PalaceNatural numbersRecursive equations become executableArithmetic pays in successor steps
Five languages, five decisions about what is allowed to exist.

Functions before names#

Functional() reduces the visible language to identifiers and argument lists. The first six distinct identifiers encountered become six native functions, in order: zero, one, equality, assignment, scoped variable binding, and function construction. The next three names claim standard input and output. The spellings carry no meaning — whatever identifier arrives first is zero.

functional() semantic prelude
zero, one,equal, assign, variable, function,read, write, eof,variable(not, function(x)(equal(x, zero)))

In this prelude, zero means zero because it happens to be the first new identifier, not because the parser recognizes the word. Once the six primitives exist, the language can build user functions, closures, variables, and eventually object-like structures, and the implementation takes all of this seriously: tokenizer, parser, compiler, machine, and bit-level input and output each live in their own module.

The bootstrap is the good part. Functional() shows how little machinery higher-order programming actually needs once function creation and variable binding exist.

The bad part is that naming becomes global state. Insert an unfamiliar identifier before the prelude and every primitive after it may change identity, so a refactor can silently change which language the rest of the file is written in. I reread the specification twice expecting that to be my misunderstanding, and it isn't — the arrival-order rule really is the whole linking model.

Bits without a border#

Bitwise Trance stores the source itself in an unbounded, zero-filled bit memory. Every instruction reads one test address followed by two opcode-and-address branches; the bit at the test address chooses which branch runs. Four opcodes cover jump, bit flip, input, and output, and that is the entire instruction set.

one decoded Bitwise Trance instruction
01010100001011test address 00 -> input one bit into address 31 -> jump to bit address 5

The fourteen source bits above decode to a test of address zero: on a zero, read an input bit into address three; on a one, jump to bit address five. Nothing requires the jump target to be an instruction boundary, and the flip operation can mutate source bits, so code, data, branch table, and parser alignment all share one memory — the language never promised they were different things.

That collapse is the useful idea, though. One instruction bundles an observation with the two actions it selects between, and that's the whole theory of control flow. The interpreter core fits in a small reader and a four-case switch, so you can check the strange semantics against the code directly.

The cost is locality. Once a jump lands inside an old instruction, or a flip changes its encoding, whatever disassembly you had in your head is fiction. The host leaks in too: the specification describes unbounded addresses, but the JavaScript reader refuses any address with thirty continuation groups because its bitwise arithmetic can't carry the abstraction that far, and the runner has to invent an output convention for stopping, since the language never included a halt instruction.

Sets without sequence#

ZFC++ allows finite sets, function definitions, calls, and two odd operators. Inversion, !, maps the empty set to a singleton and every nonempty set to the empty set. Spread, ~, applies a function across the members of a set and unions the returned sets. There are no built-in numbers, booleans, lists, or strings; everything has to become a set first.

zfc++
zero: {}one: {zero}not(x): !xbool(x): not(not(x))union(x): collect(~x)collect(x): x

Empty and singleton sets stand in for false and true, and double inversion converts any set into that boolean pair. collect(~x) calls collect for each member of x and unions what comes back, which turns out to be enough to define union without a native keyword for it.

The part that impressed me sits in the set implementation: it recursively canonicalizes members, removes duplicates, and sorts by structural identity, so set equality behaves extensionally. Order and repetition disappear because the language said they don't matter, and the interpreter actually enforces that.

The same commitment is what hurts. Sequence and multiplicity are ordinary programming needs, so every list, string, and bag has to be encoded, canonicalization re-walks structure constantly, and one spread call can fan out into a call per member before the results union back together. I haven't measured how badly that compounds, but nothing in the design suggests it stays polite.

The machine eats its own diagram#

The Self-modifying Turing machine keeps source, transition table, current state, data pointer, working data, input, and output on one zero-indexed bit tape. Even the description of how to execute the tape lives on the tape.

conceptual tape layout
memory[0]     halt flagmemory[1]     even-or-odd description bankmemory[...]   state width and transition cardsmemory[...]   mutable data pointermemory[...]   data and tagged input bits

At each step, bit one selects the even or odd lane from which the machine description is read; the interpreter then decodes the state width, current state, transition cards, and mutable pointer. A selected card may flip the pointed bit, move the pointer, and choose another state. If the pointer wanders into metadata, the next transition can rewrite the machine that chooses transitions.

This is the cleanest self-modification in the cabinet — no privileged instruction segment pretending to be immutable while data lives somewhere else. The 131-line interpreter re-reads the description from sparse BigInt-addressed memory on every single cycle, which is slow and honest: it pays for the premise instead of compiling it away.

It also means you can't reason locally about anything. A one-bit write may alter a value, the pointer encoding, a branch card, the parser lane, or the halt flag, and the published cat program can halt after a single transition because encoded input already sits in the tape region used for output. Elegant in the same way a room with no walls is spacious. The specification also stops short of a Turing-completeness proof, so the most machine-like language here leaves its own computational class open.

Arithmetic one successor at a time#

Palace permits natural numbers, zero, function calls, and a prefix successor marker. Function equations pattern-match on zero or on a successor value; the first function is the entry point, and its arguments come from the input integers.

palace
add(x, 0) = xadd(x, +y) = add(+x, y)mul(x, 0) = 0mul(x, +y) = add(x, mul(x, y))

The first pair of equations is addition done as a proof by cases: adding zero returns the other argument, and adding a successor moves one successor across and recurses. Multiplication repeats addition by the same pattern. Nothing hides the induction.

Palace is the one I'd teach. The equations read cleanly, and the implementation does more than blind dispatch — it compiles patterns into a decision structure, rejects ambiguous overloads, detects missing cases, and checks identifiers before running anything. Values live as BigInt in the host, so the interpreter isn't literally allocating chains of successor nodes.

BigInt doesn't erase the semantic bill, though. Addition still peels one successor at a time, multiplication nests that process, and negative numbers, products, sequences, and effects don't exist until you encode them or change the language. You end up watching arithmetic happen at a speed where you can follow it, which I suspect was the goal.

What the drawer keeps#

Line the five up and each has removed a different comfort. Functional() destabilizes primitive names, Bitwise Trance destabilizes instruction boundaries, ZFC++ drops order and multiplicity, the Self-modifying Turing machine drops the wall between machine and memory, and Palace gives up every arithmetic shortcut. None of them would survive contact with a real project, and none of them is trying to.

What makes the archive worth an afternoon is that the implementations follow the ideas farther than a novelty syntax usually survives. The sets get canonicalized, the pattern equations get checked for ambiguity, the machine description really is mutable tape. The weak points show up exactly where the host language intrudes — finite JavaScript bitwise addresses, BigInt representations, a termination convention bolted on from outside — and I trust a semantic claim more once I've watched an interpreter forced to pay for it.

Mainstream languages need several models because programs hold several kinds of work, and this archive refuses that compromise five separate ways. I don't expect Thomas to come back to it — archived means archived — but before closing the tab I starred the repository. Fifteen now.