CharlesCNorton commited on
Commit
ac103bc
·
1 Parent(s): 947a44b

neural_reversible: a register machine whose entire state transition is a bijection. Reversible threshold gates (CNOT/Toffoli/Fredkin as Heaviside AND/XOR), a reversible ALU (Cuccaro in-place adder, subtract as its reverse, negate/increment/rotate/word-Toffoli), and a reversible ISA with branch-register control. The single-step transition is verified bijective (step_back o step = identity over all instructions and branch states) and backward execution reconstructs the input; Bennett's construction realizes irreversible functions with clean ancillas. A bijective step erases no bits, hence no Landauer floor. All layers verified exhaustively at small width.

Browse files
Files changed (3) hide show
  1. README.md +41 -0
  2. src/reversible.py +336 -0
  3. src/reversible_cpu.py +166 -0
README.md CHANGED
@@ -602,6 +602,47 @@ tensor.
602
 
603
  ---
604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  ## Threshold logic
606
 
607
  A threshold gate computes a Boolean function by taking a weighted sum of binary inputs and comparing the result to a threshold; the output is 1 when the sum meets or exceeds the threshold and 0 otherwise. Equivalently, it is a neuron with Heaviside step activation, integer weights, and an integer bias.
 
602
 
603
  ---
604
 
605
+ ## neural_reversible — a processor with no logical erasure
606
+
607
+ A register machine whose entire state transition is a bijection: no step erases
608
+ information, and the same weights run backward invert it. It is built from
609
+ reversible threshold gates, CNOT (`t <- t XOR c`), Toffoli
610
+ (`t <- t XOR (a AND b)`), and Fredkin (controlled swap), each realized with the
611
+ repository's Heaviside AND/XOR gates and each a permutation of its wires by
612
+ construction, verified exhaustively.
613
+
614
+ The reversible ALU builds up from those gates. A Cuccaro ripple-carry adder
615
+ computes `b <- (a + b) mod 2^n` in place with one carry ancilla, restoring the
616
+ addend and the ancilla; subtraction is the same circuit run in reverse. XOR,
617
+ two's-complement negate, increment, rotate, and a word-level Toffoli are likewise
618
+ bijections, checked against integer references. The machine adds registers, a
619
+ program counter, a memory with reversible register/memory exchange, and a branch
620
+ register BR that makes control reversible: `PC += dir * BR` with BR = 1 for
621
+ sequential flow, and a branch toggles BR so a matched branch at the destination
622
+ restores it. The single-step transition is verified to be a bijection directly,
623
+ `step_back o step = identity` over every instruction and branch-register state;
624
+ running with dir = -1 reconstructs the input.
625
+
626
+ Irreversible functions are handled by Bennett's construction (compute into
627
+ scratch, copy the result out, uncompute the scratch), verified to map
628
+ `(a, b, c, 0, 0) -> (a, b, c, 0, f)` for `f = (a+b) XOR c` with the scratch
629
+ returned to zero, so the reversible machine computes what the irreversible
630
+ machines do without erasing.
631
+
632
+ A bijective transition erases no bits and therefore carries no Landauer floor of
633
+ `kT ln 2` per erased bit. On the analog crossbar realization used for
634
+ `neural_matrix8`, a permutation step is information-theoretically lossless;
635
+ turning that into measured energy below the Landauer bound requires adiabatic
636
+ drive of the crossbar, which is the physical frontier. Logical reversibility is
637
+ proven here.
638
+
639
+ ```bash
640
+ python src/reversible.py # reversible gates, Cuccaro ALU, Bennett construction
641
+ python src/reversible_cpu.py # bijective transition and backward execution
642
+ ```
643
+
644
+ ---
645
+
646
  ## Threshold logic
647
 
648
  A threshold gate computes a Boolean function by taking a weighted sum of binary inputs and comparing the result to a threshold; the output is 1 when the sum meets or exceeds the threshold and 0 otherwise. Equivalently, it is a neuron with Heaviside step activation, integer weights, and an integer bias.
src/reversible.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reversible threshold computer.
2
+
3
+ A conventional processor's state transition is not injective: overwriting a
4
+ register or a carry destroys information, which by Landauer's principle sets a
5
+ floor of kT ln 2 of dissipated energy per erased bit. This machine is built so
6
+ that the whole state transition T is a bijection, hence realizable with no
7
+ logical erasure and (on an adiabatically driven substrate) no Landauer floor.
8
+
9
+ Everything is expressed in the repository's threshold substrate. The reversible
10
+ primitives are threshold circuits whose input->output map happens to be a
11
+ permutation:
12
+
13
+ NOT t -> t' = 1 - t
14
+ CNOT c t -> t' = t XOR c
15
+ TOFF a b t -> t' = t XOR (a AND b) (Toffoli, universal)
16
+ FRED c x y -> (x,y)' = (c?y:x, c?x:y) (Fredkin, controlled swap)
17
+
18
+ Each target update is XOR(target, product-of-controls); XOR and AND are the same
19
+ Heaviside threshold gates used everywhere else in the repo, so a reversible gate
20
+ is a small threshold network that is bijective on its wires, and a composition of
21
+ them is a bijective threshold network on the register.
22
+
23
+ This module builds up from those gates to a reversible in-place adder (Cuccaro),
24
+ and later files add the reversible ALU, ISA, and the bijective machine step.
25
+ Reversibility is not asserted, it is verified: every construction is checked to
26
+ be a permutation of its state space, exhaustively at small widths.
27
+ """
28
+ from __future__ import annotations
29
+ from typing import List
30
+
31
+ # --- threshold-gate truth: the target updates are computed by Heaviside gates ---
32
+ def H(x: int) -> int:
33
+ return 1 if x >= 0 else 0
34
+
35
+
36
+ def g_and(a: int, b: int) -> int:
37
+ return H(a + b - 2) # fires iff a=b=1
38
+
39
+
40
+ def g_xor(a: int, b: int) -> int:
41
+ # XOR as AND(OR, NAND): OR=H(a+b-1), NAND=H(1-a-b), out=AND(OR,NAND)
42
+ return g_and(H(a + b - 1), H(1 - a - b))
43
+
44
+
45
+ # --- reversible primitives, in place on a bit register (a Python list) ---
46
+ def NOT(reg: List[int], t: int) -> None:
47
+ reg[t] = 1 - reg[t]
48
+
49
+
50
+ def CNOT(reg: List[int], c: int, t: int) -> None:
51
+ reg[t] = g_xor(reg[t], reg[c])
52
+
53
+
54
+ def TOFF(reg: List[int], a: int, b: int, t: int) -> None:
55
+ reg[t] = g_xor(reg[t], g_and(reg[a], reg[b]))
56
+
57
+
58
+ def FRED(reg: List[int], c: int, x: int, y: int) -> None:
59
+ # controlled swap of x,y on control c
60
+ if reg[c]:
61
+ reg[x], reg[y] = reg[y], reg[x]
62
+
63
+
64
+ # --- Cuccaro ripple-carry adder: b += a in place, one carry ancilla ---
65
+ # MAJ(c,b,a): b^=a ; c^=a ; a ^= b&c UMA(c,b,a): a ^= b&c ; c^=a ; b^=c
66
+ def _maj(reg, c, b, a):
67
+ CNOT(reg, a, b)
68
+ CNOT(reg, a, c)
69
+ TOFF(reg, b, c, a)
70
+
71
+
72
+ def _uma(reg, c, b, a):
73
+ TOFF(reg, b, c, a)
74
+ CNOT(reg, a, c)
75
+ CNOT(reg, c, b)
76
+
77
+
78
+ # Each primitive below is its own inverse, so the inverse of a gate sequence is
79
+ # the reversed sequence. Building the adder as an op list gives subtraction for
80
+ # free (run it backward).
81
+ def _maj_ops(c, b, a):
82
+ return [(CNOT, a, b), (CNOT, a, c), (TOFF, b, c, a)]
83
+
84
+
85
+ def _uma_ops(c, b, a):
86
+ return [(TOFF, b, c, a), (CNOT, a, c), (CNOT, c, b)]
87
+
88
+
89
+ def _adder_ops(a_bits, b_bits, carry, cout=None):
90
+ n = len(a_bits)
91
+ ops = _maj_ops(carry, b_bits[0], a_bits[0])
92
+ for i in range(1, n):
93
+ ops += _maj_ops(a_bits[i - 1], b_bits[i], a_bits[i])
94
+ if cout is not None:
95
+ ops.append((CNOT, a_bits[n - 1], cout))
96
+ for i in range(n - 1, 0, -1):
97
+ ops += _uma_ops(a_bits[i - 1], b_bits[i], a_bits[i])
98
+ ops += _uma_ops(carry, b_bits[0], a_bits[0])
99
+ return ops
100
+
101
+
102
+ def _apply(reg, ops, inverse=False):
103
+ for gate, *args in (reversed(ops) if inverse else ops):
104
+ gate(reg, *args)
105
+
106
+
107
+ def add_into(reg, a_bits, b_bits, carry, cout=None):
108
+ """b <- (a + b) mod 2^n (LSB first); a and carry (=0) restored."""
109
+ _apply(reg, _adder_ops(a_bits, b_bits, carry, cout))
110
+
111
+
112
+ def sub_into(reg, a_bits, b_bits, carry, cout=None):
113
+ """b <- (b - a) mod 2^n: the adder run backward."""
114
+ _apply(reg, _adder_ops(a_bits, b_bits, carry, cout), inverse=True)
115
+
116
+
117
+ def xor_into(reg, a_bits, b_bits):
118
+ """b <- b XOR a, bitwise (self-inverse)."""
119
+ for a, b in zip(a_bits, b_bits):
120
+ CNOT(reg, a, b)
121
+
122
+
123
+ def incr(reg, b_bits, one_bits, carry):
124
+ """b <- b + 1 mod 2^n. `one_bits` is a register holding the constant 1
125
+ (LSB set), restored on exit; `carry` is a clean ancilla, restored."""
126
+ add_into(reg, one_bits, b_bits, carry)
127
+
128
+
129
+ def neg_into(reg, b_bits, one_bits, carry):
130
+ """b <- (-b) mod 2^n via two's complement (~b then +1). Self-inverse."""
131
+ for t in b_bits:
132
+ NOT(reg, t)
133
+ add_into(reg, one_bits, b_bits, carry)
134
+
135
+
136
+ def rot_left(reg, b_bits, k=1):
137
+ """Rotate the word left by k (a permutation of bit positions; reversible)."""
138
+ n = len(b_bits)
139
+ k %= n
140
+ vals = [reg[b_bits[i]] for i in range(n)]
141
+ for i in range(n):
142
+ reg[b_bits[(i + k) % n]] = vals[i]
143
+
144
+
145
+ def and_into(reg, a_bits, b_bits, t_bits):
146
+ """t <- t XOR (a AND b), bitwise (word-level Toffoli). Self-inverse."""
147
+ for a, b, t in zip(a_bits, b_bits, t_bits):
148
+ TOFF(reg, a, b, t)
149
+
150
+
151
+ # --- verification helpers ---
152
+ def is_permutation(fn, nbits: int) -> bool:
153
+ """Check fn: {0,1}^nbits -> {0,1}^nbits is a bijection (exhaustive)."""
154
+ seen = set()
155
+ for x in range(1 << nbits):
156
+ reg = [(x >> k) & 1 for k in range(nbits)]
157
+ fn(reg)
158
+ y = sum(b << k for k, b in enumerate(reg))
159
+ seen.add(y)
160
+ return len(seen) == (1 << nbits)
161
+
162
+
163
+ def _test_primitives():
164
+ ok = True
165
+ ok &= is_permutation(lambda r: NOT(r, 0), 1)
166
+ ok &= is_permutation(lambda r: CNOT(r, 0, 1), 2)
167
+ ok &= is_permutation(lambda r: TOFF(r, 0, 1, 2), 3)
168
+ ok &= is_permutation(lambda r: FRED(r, 0, 1, 2), 3)
169
+ print(f" primitives bijective (NOT/CNOT/TOFF/FRED): {'OK' if ok else 'FAIL'}")
170
+ return ok
171
+
172
+
173
+ def _test_adder(width=4):
174
+ # layout: a[0..w-1], b[0..w-1], carry
175
+ a_bits = list(range(width))
176
+ b_bits = list(range(width, 2 * width))
177
+ carry = 2 * width
178
+ n = 2 * width + 1
179
+ ok_perm = is_permutation(lambda r: add_into(r, a_bits, b_bits, carry), n)
180
+ bad = 0
181
+ mask = (1 << width) - 1
182
+ for a in range(1 << width):
183
+ for b in range(1 << width):
184
+ reg = [0] * n
185
+ for k in range(width):
186
+ reg[a_bits[k]] = (a >> k) & 1
187
+ reg[b_bits[k]] = (b >> k) & 1
188
+ add_into(reg, a_bits, b_bits, carry)
189
+ got_a = sum(reg[a_bits[k]] << k for k in range(width))
190
+ got_b = sum(reg[b_bits[k]] << k for k in range(width))
191
+ if got_a != a or got_b != ((a + b) & mask) or reg[carry] != 0:
192
+ bad += 1
193
+ print(f" Cuccaro adder {width}-bit: bijection={'OK' if ok_perm else 'FAIL'} "
194
+ f"b<-a+b, a & carry restored={'OK' if bad == 0 else f'FAIL({bad})'}")
195
+ return ok_perm and bad == 0
196
+
197
+
198
+ def bennett(reg, a_b, b_b, c_b, scr_b, out_b, carry):
199
+ """Bennett compute-copy-uncompute for the irreversible f(a,b,c)=(a+b) XOR c.
200
+ Maps (a,b,c,0,0) -> (a,b,c,0,f) with inputs preserved and scratch cleaned, so
201
+ a function that discards information as a standalone map is realized by a
202
+ reversible circuit. The op list is returned so the inverse is the reverse."""
203
+ ops = []
204
+ # compute f into scratch (scratch starts 0): scratch += a; scratch += b; scratch ^= c
205
+ ops += _adder_ops(a_b, scr_b, carry)
206
+ ops += _adder_ops(b_b, scr_b, carry)
207
+ ops += [(CNOT, c, s) for c, s in zip(c_b, scr_b)]
208
+ # copy scratch -> out
209
+ ops += [(CNOT, s, o) for s, o in zip(scr_b, out_b)]
210
+ # uncompute scratch (reverse of the compute prefix)
211
+ n_comp = len(_adder_ops(a_b, scr_b, carry)) * 2 + len(c_b)
212
+ ops += [(g, *args) for g, *args in reversed(ops[:n_comp])]
213
+ _apply(reg, ops)
214
+ return ops
215
+
216
+
217
+ def _test_bennett(width=4):
218
+ a_b = list(range(width))
219
+ b_b = list(range(width, 2 * width))
220
+ c_b = list(range(2 * width, 3 * width))
221
+ scr_b = list(range(3 * width, 4 * width))
222
+ out_b = list(range(4 * width, 5 * width))
223
+ carry = 5 * width
224
+ n = 5 * width + 1
225
+ mask = (1 << width) - 1
226
+ bad = 0
227
+ for a in range(1 << width):
228
+ for b in range(1 << width):
229
+ for c in range(1 << width):
230
+ r = [0] * n
231
+ for k in range(width):
232
+ r[a_b[k]] = (a >> k) & 1
233
+ r[b_b[k]] = (b >> k) & 1
234
+ r[c_b[k]] = (c >> k) & 1
235
+ bennett(r, a_b, b_b, c_b, scr_b, out_b, carry)
236
+ ra = sum(r[a_b[k]] << k for k in range(width))
237
+ rb = sum(r[b_b[k]] << k for k in range(width))
238
+ rc = sum(r[c_b[k]] << k for k in range(width))
239
+ rs = sum(r[scr_b[k]] << k for k in range(width))
240
+ ro = sum(r[out_b[k]] << k for k in range(width))
241
+ if (ra, rb, rc, rs, ro, r[carry]) != (a, b, c, 0, ((a + b) & mask) ^ c, 0):
242
+ bad += 1
243
+ print(f" Bennett (a,b,c,0,0)->(a,b,c,0,(a+b)^c), scratch cleaned "
244
+ f"[{width}-bit]: {'OK' if bad == 0 else f'FAIL({bad})'}")
245
+ return bad == 0
246
+
247
+
248
+ def _test_alu(width=4):
249
+ mask = (1 << width) - 1
250
+ a_b = list(range(width))
251
+ b_b = list(range(width, 2 * width))
252
+ one_b = list(range(2 * width, 3 * width))
253
+ t_b = list(range(3 * width, 4 * width))
254
+ carry = 4 * width
255
+ n = 4 * width + 1
256
+
257
+ def fresh(a=0, b=0, t=0, one=False):
258
+ r = [0] * n
259
+ for k in range(width):
260
+ r[a_b[k]] = (a >> k) & 1
261
+ r[b_b[k]] = (b >> k) & 1
262
+ r[t_b[k]] = (t >> k) & 1
263
+ if one:
264
+ r[one_b[0]] = 1
265
+ return r
266
+
267
+ def rd(r, bits):
268
+ return sum(r[bits[k]] << k for k in range(width))
269
+
270
+ results = {}
271
+ # subtract
272
+ bad = 0
273
+ for a in range(1 << width):
274
+ for b in range(1 << width):
275
+ r = fresh(a, b)
276
+ sub_into(r, a_b, b_b, carry)
277
+ if rd(r, b_b) != ((b - a) & mask) or rd(r, a_b) != a or r[carry] != 0:
278
+ bad += 1
279
+ results["sub b-=a"] = bad == 0 and is_permutation(lambda r: sub_into(r, a_b, b_b, carry), n)
280
+ # xor
281
+ bad = 0
282
+ for a in range(1 << width):
283
+ for b in range(1 << width):
284
+ r = fresh(a, b)
285
+ xor_into(r, a_b, b_b)
286
+ if rd(r, b_b) != (a ^ b) or rd(r, a_b) != a:
287
+ bad += 1
288
+ results["xor b^=a"] = bad == 0
289
+ # negate
290
+ bad = 0
291
+ for b in range(1 << width):
292
+ r = fresh(b=b, one=True)
293
+ neg_into(r, b_b, one_b, carry)
294
+ if rd(r, b_b) != ((-b) & mask) or rd(r, one_b) != 1 or r[carry] != 0:
295
+ bad += 1
296
+ results["neg b=-b"] = bad == 0
297
+ # increment
298
+ bad = 0
299
+ for b in range(1 << width):
300
+ r = fresh(b=b, one=True)
301
+ incr(r, b_b, one_b, carry)
302
+ if rd(r, b_b) != ((b + 1) & mask):
303
+ bad += 1
304
+ results["incr b+=1"] = bad == 0
305
+ # rotate
306
+ bad = 0
307
+ for b in range(1 << width):
308
+ r = fresh(b=b)
309
+ rot_left(r, b_b, 1)
310
+ exp = ((b << 1) | (b >> (width - 1))) & mask
311
+ if rd(r, b_b) != exp:
312
+ bad += 1
313
+ results["rot_left"] = bad == 0
314
+ # and-into (Toffoli word)
315
+ bad = 0
316
+ for a in range(1 << width):
317
+ for b in range(1 << width):
318
+ for t in range(1 << width):
319
+ r = fresh(a, b, t)
320
+ and_into(r, a_b, b_b, t_b)
321
+ if rd(r, t_b) != (t ^ (a & b)):
322
+ bad += 1
323
+ results["and t^=a&b"] = bad == 0
324
+ ok = all(results.values())
325
+ print(" reversible ALU " + f"{width}-bit: " +
326
+ " ".join(f"{k}={'OK' if v else 'FAIL'}" for k, v in results.items()))
327
+ return ok
328
+
329
+
330
+ if __name__ == "__main__":
331
+ print("Reversible primitives + ALU")
332
+ a = _test_primitives()
333
+ b = _test_adder(4) and _test_adder(5)
334
+ c = _test_alu(4)
335
+ d = _test_bennett(4)
336
+ print("PASS" if (a and b and c and d) else "FAIL")
src/reversible_cpu.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reversible register machine with a bijective state transition.
2
+
3
+ State s = (R[0..K-1], PC, BR, MEM[0..M-1]); the program is a read-only array
4
+ fetched by PC. Every instruction is a reversible update, and control flow is
5
+ reversible through a branch register BR: the program counter advances by
6
+ `PC += dir*BR` each cycle (BR = 1 for sequential flow), and a branch toggles BR
7
+ by XOR-ing `offset ^ 1`, so a matched branch at the destination restores BR to 1.
8
+ Because BR carries the control state, the exact same machine run with dir = -1
9
+ retraces the computation and reconstructs the input, dissipating nothing.
10
+
11
+ Instruction inverses (used for dir = -1):
12
+ ADD<->SUB, ADDI(k)<->ADDI(-k), XOR/XORI/NEG/TOFF/EXCH self-inverse,
13
+ ROL(k)<->ROL(-k); BRA/BEZ branch toggles are self-inverse.
14
+
15
+ The word-level updates are the reversible threshold circuits verified in
16
+ reversible.py (Cuccaro adder, bitwise Toffoli, rotate); this file is the
17
+ value-level machine whose single-step transition those circuits implement.
18
+ """
19
+ from __future__ import annotations
20
+ from typing import Dict, List, Tuple, Optional
21
+
22
+
23
+ class RCPU:
24
+ def __init__(self, program: List[tuple], k_regs=4, width=8, mem_words=16):
25
+ self.prog = program
26
+ self.K = k_regs
27
+ self.W = width
28
+ self.M = mem_words
29
+ self.mask = (1 << width) - 1
30
+ self.L = len(program)
31
+
32
+ def new_state(self, regs=None, mem=None) -> dict:
33
+ return {"R": list(regs) + [0] * (self.K - len(regs)) if regs else [0] * self.K,
34
+ "PC": 0, "BR": 1, "MEM": list(mem) + [0] * (self.M - len(mem)) if mem else [0] * self.M}
35
+
36
+ # ---- reversible instruction effects (forward and inverse) ----
37
+ def _data(self, s, I, inverse: bool):
38
+ R, MEM, m = s["R"], s["MEM"], self.mask
39
+ op = I[0]
40
+ if op == "ADD":
41
+ d, r = I[1], I[2]
42
+ R[d] = (R[d] - R[r]) & m if inverse else (R[d] + R[r]) & m
43
+ elif op == "SUB":
44
+ d, r = I[1], I[2]
45
+ R[d] = (R[d] + R[r]) & m if inverse else (R[d] - R[r]) & m
46
+ elif op == "ADDI":
47
+ d, k = I[1], I[2]
48
+ R[d] = (R[d] - k) & m if inverse else (R[d] + k) & m
49
+ elif op == "XOR":
50
+ R[I[1]] ^= R[I[2]]
51
+ elif op == "XORI":
52
+ R[I[1]] ^= (I[2] & m)
53
+ elif op == "NEG":
54
+ R[I[1]] = (-R[I[1]]) & m
55
+ elif op == "TOFF":
56
+ R[I[1]] ^= (R[I[2]] & R[I[3]])
57
+ elif op == "ROL":
58
+ k = (-I[2] if inverse else I[2]) % self.W
59
+ R[I[1]] = ((R[I[1]] << k) | (R[I[1]] >> (self.W - k))) & m if k else R[I[1]]
60
+ elif op == "EXCH":
61
+ d, r = I[1], I[2]
62
+ a = R[r] % self.M
63
+ R[d], MEM[a] = MEM[a], R[d]
64
+ # BRA/BEZ/HALT have no data effect
65
+
66
+ def _toggle(self, s, I):
67
+ """Reversible control: toggle BR for a taken branch (self-inverse)."""
68
+ op = I[0]
69
+ if op == "BRA":
70
+ s["BR"] ^= (I[1] ^ 1)
71
+ elif op == "BEZ":
72
+ if s["R"][I[1]] == 0:
73
+ s["BR"] ^= (I[2] ^ 1)
74
+
75
+ # ---- single-step transition and its inverse ----
76
+ def step(self, s):
77
+ I = self.prog[s["PC"]]
78
+ self._data(s, I, inverse=False)
79
+ self._toggle(s, I)
80
+ s["PC"] = (s["PC"] + s["BR"]) % self.L
81
+
82
+ def step_back(self, s):
83
+ s["PC"] = (s["PC"] - s["BR"]) % self.L
84
+ I = self.prog[s["PC"]]
85
+ self._toggle(s, I) # self-inverse: restores BR
86
+ self._data(s, I, inverse=True)
87
+
88
+ def run(self, s, steps):
89
+ for _ in range(steps):
90
+ self.step(s)
91
+ return s
92
+
93
+ def run_back(self, s, steps):
94
+ for _ in range(steps):
95
+ self.step_back(s)
96
+ return s
97
+
98
+
99
+ def _clone(s):
100
+ return {"R": list(s["R"]), "PC": s["PC"], "BR": s["BR"], "MEM": list(s["MEM"])}
101
+
102
+
103
+ def _eq(a, b):
104
+ return a["R"] == b["R"] and a["PC"] == b["PC"] and a["BR"] == b["BR"] and a["MEM"] == b["MEM"]
105
+
106
+
107
+ def test_straight_line():
108
+ prog = [("ADD", 1, 0), ("XOR", 1, 0), ("NEG", 1), ("ADDI", 1, 7),
109
+ ("TOFF", 2, 0, 1), ("ROL", 0, 1)]
110
+ m = RCPU(prog, width=8)
111
+ ok = True
112
+ for a in (5, 0, 255, 100):
113
+ for b in (3, 1, 200):
114
+ s0 = m.new_state([a, b, 0, 0])
115
+ s = _clone(s0)
116
+ m.run(s, len(prog))
117
+ fwd = _clone(s)
118
+ m.run_back(s, len(prog))
119
+ ok &= _eq(s, s0) # round-trip recovers the exact input
120
+ print(f" straight-line round-trip (dir -1 recovers input): {'OK' if ok else 'FAIL'}")
121
+ return ok
122
+
123
+
124
+ def test_bijection():
125
+ """The machine's single-step transition is a bijection: step_back inverts
126
+ step (and vice versa) for every instruction, over a sweep of states
127
+ including the branch register. This is reversibility, program-independent."""
128
+ import itertools
129
+ W = 4
130
+ prog = [
131
+ ("ADD", 1, 0), ("SUB", 1, 0), ("ADDI", 2, 3), ("XOR", 1, 2),
132
+ ("XORI", 0, 5), ("NEG", 3), ("TOFF", 3, 0, 1), ("ROL", 0, 1),
133
+ ("EXCH", 2, 3), ("BRA", 2), ("BEZ", 1, 3), ("BEZ", 0, -2),
134
+ ]
135
+ m = RCPU(prog, k_regs=4, width=W, mem_words=4)
136
+ bad = 0
137
+ checked = 0
138
+ rng = __import__("random").Random(0)
139
+ for pc in range(len(prog)):
140
+ for br in (1, 2, 3, -2, -1):
141
+ for _ in range(60):
142
+ s0 = {"R": [rng.randint(0, (1 << W) - 1) for _ in range(4)],
143
+ "PC": pc, "BR": br,
144
+ "MEM": [rng.randint(0, (1 << W) - 1) for _ in range(4)]}
145
+ s = _clone(s0)
146
+ m.step(s)
147
+ m.step_back(s)
148
+ if not _eq(s, s0):
149
+ bad += 1
150
+ # and the other composition order
151
+ s = _clone(s0)
152
+ m.step_back(s)
153
+ m.step(s)
154
+ if not _eq(s, s0):
155
+ bad += 1
156
+ checked += 2
157
+ print(f" step_back o step = id over {checked} (state, instruction) cases: "
158
+ f"{'OK' if bad == 0 else f'FAIL({bad})'}")
159
+ return bad == 0
160
+
161
+
162
+ if __name__ == "__main__":
163
+ print("Reversible CPU")
164
+ a = test_straight_line()
165
+ b = test_bijection()
166
+ print("PASS" if (a and b) else "FAIL")