An experimental reversible programming language with a time-travel REPL, written in Go. Short name: kraM.
Every change to state is recorded with its inverse, so you can step backwards through anything you run. A subset of the language is reversible by construction — programs that can run backward by inverting their own text — which is the groundwork for the long-term goal: compiling to reversible / adiabatic circuits.
Dynamically typed, imperative, tree-walk interpreter. A research toy, early days.
make build # builds two binaries: kram and krapl
./krapl # open the REPL
./kram fib.kr # run a .kr script(Or without building: go run . fib.kr, and go run . for the REPL.)
> x = 2 + 3 * 4
14
> print "hello, " + "world"
hello, world
krapl opens the interactive REPL; kram file.kr runs a script quietly. They
are the same binary, dispatched on the invoked name.
= introduces a name; it never overwrites. Re-binding an existing variable
would destroy its value — the irreversible act — so it is an error, not a warning
(the full-Janus discipline: no destructive assignment):
> x = 5
> x = 9
error: cannot reassign "x" with '=' — '=' introduces a fresh name;
use += / -= / ^= / <=> to change it, or `forget x` to erase it first
Reversible updates keep the information and can be undone exactly:
x += 3 # inverse: x -= 3
x ^= 10 # XOR — self-inverse
a <=> b # swap — self-inverse
x *= 3 # inverse: x /= 3 — integers only, factor must be nonzero
x /= 3 # inverse: x *= 3 — must divide evenly, else a runtime error
x <<= 4 # rotate left on the 16-bit circuit word — inverse: x >>= 4
The scale pair is exact by construction: *= 0 and an inexact /= are
runtime errors, because both would destroy information. Rotations are bit
permutations (amounts wrap mod 16; the value must fit the 16-bit word), so
they lower to the gate level as a fixed swap network of CNOTs. *= / /=
stay interpreter-only — multiplication by an even factor is not a bijection
mod 2^16, so there is no general gate lowering.
To destroy information on purpose, there is one explicit escape hatch —
forget x. It erases the variable (freeing the name to re-introduce). It is the
only irreversible act in the language, so it is named and visible rather than
hiding inside an innocent-looking =; reverse / uncall refuse it and it
lowers to no gate:
> x = 5
> forget x # deliberate erasure — the one irreversible operation
> x = 9 # the name is free again
So you can run a block backward:
> a = 0
> reverse { a += 5; a += 3 } # runs a -= 3; a -= 5
> print a
0
Arrays support the same reversible element operations (reverse.kr reverses an
array in place, then undoes it):
> xs = [1, 2, 3]
> xs[0] += 10 # element update
> xs[0] <=> xs[2] # element swap
> xs
[3, 2, 11]
Name a reversible block as a procedure (with by-reference parameters), then run
it either direction — call forward, uncall backward:
> proc add(dst, src) { dst += src }
> call add(x, y) # x += y
> uncall add(x, y) # x -= y — the same procedure, reversed
Scoped temporaries use local / delocal (the Janus discipline): local
introduces a fresh variable, delocal removes it while asserting its value.
They are exact inverses, so a temporary stays reversible — the asserted value is
what lets it be removed without leaving garbage. In circuits a local lowers to
an ancilla register (allocated, used, then uncomputed and freed):
> local t = 0
> t += x # use t
> delocal t = x # remove t, asserting it now equals x
Counted loops have sugar: for i = lo until hi { … } (upper bound exclusive)
desugars to the Janus loop local i = lo; from i == lo { } loop { …; i += 1 } until i == hi; delocal i = hi. The counter is scoped and removed at the end,
and the loop's structural inverse counts back down from hi to lo:
> for i = 0 until n { call fibstep(a, b) }
The compute-copy-uncompute pattern (Bennett's trick) has dedicated syntax:
with runs its compute block, then the body, then the compute block's
structural inverse, and delocals the ancilla — the uncompute is derived from
the program text, so it can never drift out of sync with the compute. The
compute block must be reversible; that is checked at parse time:
> with t = 0 { t += x; t += x } do { out += t }
# ≡ local t = 0; t += x; t += x; out += t; t -= x; t -= x; delocal t = 0
…and step through time:
> x = 1; x += 1; x += 1
> :history
> :undo # walk back one mutation at a time
fib.kr computes Fibonacci with only reversible updates, then runs the loop
backward to recover the inputs exactly.
make build
./kram fib.krfib(10) pair: 55, 89
reversed back to: a=0, b=1, i=0
Explore it interactively in the REPL:
./krapla = 0; b = 1; i = 0; n = 10
proc fibstep(x, y) { x += y; x <=> y }
:verify from i == 0 { } loop { call fibstep(a, b); i += 1 } until i == n
from i == 0 { } loop { call fibstep(a, b); i += 1 } until i == n
print "got " + a + ", " + b
:history # every mutation — all reversible
:verify unrolls the loop, lowers it to X / CNOT / Toffoli gates, simulates
them, and confirms the circuit matches the interpreter. :gates <same code>
prints the netlist; :undo walks the computation backward one step at a time.
Note: :verify / :gates / :circuit compile against the current variables,
so run them while the loop's start condition still holds (here, before running
the loop for real, while i == 0).
One demo, the whole language: reversible updates, a parameterized procedure, a reversible loop, time travel, and verified compilation to a reversible circuit.
Each .kr demo highlights a different facet. Run with ./kram <file>; see
docs/demos.md for a walk-through of each.
| Demo | Use-case — what it demonstrates | Compiles to a circuit? |
|---|---|---|
fib.kr |
reversible arithmetic, a parameterized procedure, a reversible loop — run forward then backward to recover the inputs | ✅ |
reverse.kr |
in-place array reversal by element swaps with computed indices | ✅ |
compute.kr |
compute → copy → uncompute with a local ancilla — the garbage-free technique of reversible computing |
✅ |
ca.kr |
a reversible Margolus cellular automaton on a grid — mix a pattern, then scrub the timeline to un-mix it exactly | ✅ (nested loops unroll) |
sort.kr |
making an irreversible algorithm (bubble sort) reversible by recording a swap trace | interpreter-only |
gcd.kr |
the same for Euclid's GCD by subtraction (branch trace) | interpreter-only |
range.kr |
counting elements in [lo, hi] — a compound condition with a count trace |
interpreter-only |
Every demo is reversible — uncall and the time-travel timeline run it
backward exactly. But not every reversible program compiles to a fixed gate
circuit. A circuit is static wiring: the compiler must know at compile time
exactly which gates fire, on which wires, in what order. What defeats that is
data-dependent control flow (sort, gcd, range): their reversible if
branches on the very values the body is changing — did this pair swap? which
operand was larger? — and gcd's loop length depends on its inputs. The gate
sequence would differ from one input to the next, so there is no single wiring;
the branch is decided at run time, and its outcome recorded in a trace. That
trace is the whole point — exactly the information the irreversible algorithm
would otherwise destroy, and what lets uncall reconstruct the input from the
output.
Everything whose control flow is fixed at compile time does lower, even when
it looks complex: the compiler unrolls loops (including nested loops, like
ca's grid sweep — each inner pass re-unrolls against the advancing compile-time
state), inlines procedures, prepares array literals element by element, and
folds loop-invariant index/bound expressions to constants. fib, reverse,
compute, and ca all lower to X / CNOT / Toffoli gates whose circuit is
checked against the interpreter (:verify) and costed with Landauer's bound
(:energy — 0 J when every local ancilla is delocal'd back to clean).
Note: the compile commands lower a whole program from cold (its =
initialisation included). A demo file that runs a computation forward and then
backward (fib.kr/reverse.kr end with a reverse { … } pass) is two passes,
not one circuit — compile the single forward pass, not the whole file.
:undo :redo :history :env — time travel and inspection
:load :step — load a program and run it one mutation at a time
:invert CODE — print a program's structural inverse
:circuit CODE — compile reversible code to a register-level netlist
:gates CODE — compile to elementary X / CNOT / Toffoli gates (adds use a Cuccaro adder)
:verify CODE — check the compiled circuit matches the interpreter
:energy CODE — Landauer energy bound from the circuit's garbage bits
:grover BITS COND [iters=K] [qasm] — Grover-search a compiled oracle (see below)
:bv BITS S [qasm] — Bernstein–Vazirani: recover hidden S in one oracle query
:dj BITS COND [qasm] — Deutsch–Jozsa: COND constant or balanced, in one query
:count BITS COND [t=K] — quantum counting: estimate how many x satisfy COND
:simon BITS S [qasm] — Simon: recover hidden XOR period S in O(BITS) queries
:shor N [a=K] [t=K] — Shor period-finding: factor toy N via QPE on a·x mod N
:qasm CODE — export a compiled program as OpenQASM 2.0
:reset :help — clear state, list commands
Shorthands: _ = last result, !! = last line (e.g. reverse { !! }).
kraM's if-condition compiler is, accidentally on purpose, a quantum oracle
synthesizer: condToBit computes any comparison/&&/||/! condition into a
single marker bit and uncomputes all of its scratch — the compute-copy-uncompute
discipline quantum oracles require. :grover exploits that:
> :grover 4 x == 9 || x == 3
oracle: x == 9 || x == 3 over 4-bit x — 47 gates, 10 wires (4 input + 1 marker + 5 ancilla)
search space N=16, solutions M=2 [3 9]
optimal iterations k* = 2
after 0 iterations: P(marked) = 0.1250 █████
after 1 iterations: P(marked) = 0.7812 ███████████████████████████████
after 2 iterations: P(marked) = 0.9453 ██████████████████████████████████████
The simulation is exact, not approximate: the oracle is verified garbage-free for every basis state and the marker is only ever a gate target, so the amplitude evolution over the 2^bits inputs equals the full statevector's (cross-checked in tests by simulating the exported circuit wire-for-wire).
Add qasm to export the complete Grover circuit — superposition layer, phase
kickback, diffusion — as OpenQASM 2.0, and run it on real IBM Quantum hardware:
see hardware/. The Studio has a matching pane: type a condition,
watch the amplitude bars converge, download the .qasm.
:bv BITS S is Bernstein–Vazirani: a hidden string S is recovered from the
linear oracle f(x) = S·x (mod 2) in a single query (classically it takes BITS
queries). The oracle is a bare CNOT chain, so unlike Grover it survives real
hardware nearly intact.
:dj BITS COND is Deutsch–Jozsa: one query decides whether COND is constant
or balanced (classically: 2^(BITS-1)+1 evaluations worst case). Measure all
zeros → constant, anything else → balanced. Conditions that are neither break
the promise; the report says so and gives the exact P(all zeros).
:count BITS COND is quantum counting: phase estimation on the Grover
operator estimates how many x satisfy COND without finding them — the
quantum sibling of range.kr's classical hit-counting. The report shows the
exact QPE readout distribution (cross-checked in tests against a direct
unitary simulation) and the M it rounds to. Simulator-only: controlled-Grover
plus QFT is far beyond today's hardware depth budgets.
:simon BITS S is Simon's algorithm: a hidden XOR period (f(x) = f(x⊕S))
recovered in O(BITS) circuit runs plus Gaussian elimination over GF(2), where
classical search needs ~2^(BITS/2) queries — the exponential separation that
led to Shor. Each run yields a random y with y·S = 0; the report shows every
equation and the elimination verdict. The oracle is all CNOTs, so qasm
exports run well on real hardware.
:shor N factors a toy composite by Shor period-finding: QPE on the
permutation |x⟩ → |a·x mod N⟩ yields readouts near 2^t·k/r, continued
fractions recover the order r, and gcd(a^(r/2) ± 1, N) splits N. The readout
distribution is exact (same closed form as :count); the classical preamble
(even, prime, perfect-power N) and the two textbook unlucky-a cases are all
reported honestly. :shor 15 → 15 = 3 × 5. More in
docs/quantum-roadmap.md.
Early sketch. :circuit is a register-level view (whole ADD / SUB / SWAP /
CNOT blocks); :gates decomposes to real elementary gates (X / CNOT / Toffoli,
16-bit registers by default — :grover/:qasm compile narrower ones so they
fit on real quantum hardware — arithmetic mod 2^width). Both unroll reversible loops and
inline procedures against the current state; :circuit doesn't yet lower a
reversible if (use :gates for that). :gates lowers procedures (inlined) and reversible ifs — conditions can
compare a variable to a constant or to another variable (== != < > <= >=),
via an equality check or a subtract-based comparator — and && || !
combinations of those — to controlled gates. Reversible loops are unrolled using the iteration count from the current state
(the circuit is specialised to that count); nested loops unroll too — each inner
pass re-unrolls against the advancing compile-time state. Ancilla wires are recycled, so a circuit's width is bounded by
peak concurrent scratch, not program length. +=/-= aren't bit-exact in the
interpreter (^= is); the gate circuit is exact mod 2^16. Array element ops
lower to circuits when the index folds to a constant at compile time — including
loop-varying indices like xs[n-1-i], which fold per unrolled iteration (each
element becomes its own register). Genuinely dynamic indexing (a runtime address
the compiler can't fold) would need a reversible address decoder and is not
lowered.