CharlesCNorton commited on
Commit
7e7dca3
·
1 Parent(s): 93586ed

neural_reflect: host a complete SUBLEQ machine (32-byte memory, fetch-execute loop) inside the interpreter, its memory accessed through indexed addressing

Browse files
Files changed (2) hide show
  1. README.md +16 -5
  2. src/reflect.py +198 -7
README.md CHANGED
@@ -536,6 +536,15 @@ regardless of gate order, so U reproduces the family's combinational evaluation.
536
  addressed span stays small as the gate count grows, so U holds a circuit far
537
  larger than its wire space. The 156-gate SUBLEQ datapath is interpreted with
538
  a 272-signal addressed span.
 
 
 
 
 
 
 
 
 
539
  - **Self-reference.** When the netlist is address-reachable, a gate can write
540
  into it: a stored gate edits the weights that define it. The index flag adds
541
  `PTR`, so a fixed-size program walks an address range; one such program
@@ -554,13 +563,15 @@ Verified against a reference interpreter: exhaustive single-gate semantics
554
  (every ternary weight pair, bias, and input assignment), single-step agreement
555
  on random full states with halted states as fixed points, bit-exact
556
  interpretation of the ripple-carry adder and the SUBLEQ datapath (all 65,536
557
- pairs) under sorted and shuffled gate order, the self-reproduction, self-rewrite,
558
- and bank-switch programs, the counter holding state through feedback, and
559
- equality of the compiled matrix form with the gate graph, the 0.5 comparator
560
- margin measured under read noise and conductance mismatch.
 
 
561
 
562
  ```bash
563
- python src/reflect.py verify # universality, order independence, quine, metamorphosis
564
  python src/reflect.py analog # matrix == gate + noise / conductance-mismatch margin
565
  ```
566
 
 
536
  addressed span stays small as the gate count grows, so U holds a circuit far
537
  larger than its wire space. The 156-gate SUBLEQ datapath is interpreted with
538
  a 272-signal addressed span.
539
+ - **A complete machine hosted inside the interpreter.** A stored microprogram
540
+ holds a whole SUBLEQ machine: its 32-byte memory lives in signals, and the
541
+ interpreter's own indexed addressing (set `PTR` to a byte address, read or
542
+ write the eight bit-planes) serves as the machine's memory-access hardware.
543
+ 266 stored gates fetch three operand bytes, read two data bytes, subtract,
544
+ write the result back, and branch, one instruction per sweep; the memory and
545
+ program counter carry across sweeps. One instruction is exact over 4,096
546
+ random `(memory, PC)` states, and a stored countdown program runs a loop to a
547
+ halt with its final memory matching the reference emulator.
548
  - **Self-reference.** When the netlist is address-reachable, a gate can write
549
  into it: a stored gate edits the weights that define it. The index flag adds
550
  `PTR`, so a fixed-size program walks an address range; one such program
 
563
  (every ternary weight pair, bias, and input assignment), single-step agreement
564
  on random full states with halted states as fixed points, bit-exact
565
  interpretation of the ripple-carry adder and the SUBLEQ datapath (all 65,536
566
+ pairs) under sorted and shuffled gate order, one hosted SUBLEQ instruction over
567
+ 4,096 random memory states and a stored program run to a halt, the
568
+ self-reproduction, self-rewrite, and bank-switch programs, the counter holding
569
+ state through feedback, and equality of the compiled matrix form with the gate
570
+ graph, the 0.5 comparator margin measured under read noise and conductance
571
+ mismatch.
572
 
573
  ```bash
574
+ python src/reflect.py verify # universality, a hosted SUBLEQ machine, quine, metamorphosis
575
  python src/reflect.py analog # matrix == gate + noise / conductance-mismatch margin
576
  ```
577
 
src/reflect.py CHANGED
@@ -457,18 +457,25 @@ def record_bit(cfg, bank, gate, off):
457
  return cfg.bank_base(bank) + gate * cfg.R + off
458
 
459
 
460
- def compile_to_reflect(cfg, net, in_addr, out_names, work_start):
 
 
 
 
 
461
  addr = dict(in_addr)
462
  w = work_start
463
  rgates = []
464
  for g in topo(net):
465
  ins, bias = net.gates[g]
466
  assert len(ins) <= cfg.F
467
- addr[g] = w; w += 1
 
 
 
468
  slots = [(addr[s], wt, 0) for s, wt in ins]
469
  rgates.append((slots, bias, (addr[g], 0)))
470
- assert len(rgates) <= cfg.G, f"{len(rgates)} gates > bank {cfg.G}"
471
- return rgates, addr
472
 
473
 
474
  def adder_net(bits):
@@ -614,7 +621,7 @@ def verify(cfg, device):
614
  print("[3/7] Universality: interpret a family full-adder cell")
615
  anet, an, bn, sums, cout = adder_net(1)
616
  ia = {an[0]: cfg.WORK_BASE, bn[0]: cfg.WORK_BASE + 1}
617
- rg, addr = compile_to_reflect(cfg, anet, ia, sums + [cout], cfg.WORK_BASE + 2)
618
  nl = encode_netlist(cfg, rg)
619
  abad = 0
620
  for x in (0, 1):
@@ -727,7 +734,7 @@ def verify_scale(device):
727
  for i in range(W):
728
  ia[an[i]] = cfg.WORK_BASE + i
729
  ia[bn[i]] = cfg.WORK_BASE + W + i
730
- rg, addr = compile_to_reflect(cfg, anet, ia, sums + [cout], cfg.WORK_BASE + 2 * W)
731
  nl = encode_netlist(cfg, rg)
732
  print(f" compiled adder netlist: {len(rg)} gates")
733
 
@@ -784,7 +791,7 @@ def verify_subleq(device):
784
  in_addr[b[k]] = base + 8 + k
785
  in_addr[pc[k]] = base + 16 + k
786
  in_addr[c[k]] = base + 24 + k
787
- rg, addr = compile_to_reflect(cfg, net, in_addr, r + [leq] + pcm, base + 32)
788
  nl = encode_netlist(cfg, rg)
789
  print(f" compiled datapath netlist: {len(rg)} gates; interpreting all "
790
  f"65,536 (M[A], M[B]) pairs")
@@ -823,6 +830,189 @@ def verify_subleq(device):
823
  return good
824
 
825
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  # =============================================================================
827
  # analog: matrix == gate + noise / conductance-mismatch margins
828
  # =============================================================================
@@ -912,6 +1102,7 @@ def main():
912
  rc |= 0 if verify(cfg, args.device) else 1
913
  rc |= 0 if verify_scale(args.device) else 1
914
  rc |= 0 if verify_subleq(args.device) else 1
 
915
  if args.cmd in ("analog", "all"):
916
  rc |= 0 if analog(cfg, args.device) else 1
917
  return rc
 
457
  return cfg.bank_base(bank) + gate * cfg.R + off
458
 
459
 
460
+ def compile_to_reflect(cfg, net, in_addr, out_names, work_start, fixed=None):
461
+ """Lay a <=2-input acyclic Net out as reflect gate records in topological
462
+ order. Primary inputs sit at in_addr; gate outputs get fresh work signals
463
+ from work_start up, except those named in `fixed` (gate -> signal), which
464
+ are placed at the given signal. Returns (records, address map, next work)."""
465
+ fixed = fixed or {}
466
  addr = dict(in_addr)
467
  w = work_start
468
  rgates = []
469
  for g in topo(net):
470
  ins, bias = net.gates[g]
471
  assert len(ins) <= cfg.F
472
+ if g in fixed:
473
+ addr[g] = fixed[g]
474
+ else:
475
+ addr[g] = w; w += 1
476
  slots = [(addr[s], wt, 0) for s, wt in ins]
477
  rgates.append((slots, bias, (addr[g], 0)))
478
+ return rgates, addr, w
 
479
 
480
 
481
  def adder_net(bits):
 
621
  print("[3/7] Universality: interpret a family full-adder cell")
622
  anet, an, bn, sums, cout = adder_net(1)
623
  ia = {an[0]: cfg.WORK_BASE, bn[0]: cfg.WORK_BASE + 1}
624
+ rg, addr, _ = compile_to_reflect(cfg,anet, ia, sums + [cout], cfg.WORK_BASE + 2)
625
  nl = encode_netlist(cfg, rg)
626
  abad = 0
627
  for x in (0, 1):
 
734
  for i in range(W):
735
  ia[an[i]] = cfg.WORK_BASE + i
736
  ia[bn[i]] = cfg.WORK_BASE + W + i
737
+ rg, addr, _ = compile_to_reflect(cfg,anet, ia, sums + [cout], cfg.WORK_BASE + 2 * W)
738
  nl = encode_netlist(cfg, rg)
739
  print(f" compiled adder netlist: {len(rg)} gates")
740
 
 
791
  in_addr[b[k]] = base + 8 + k
792
  in_addr[pc[k]] = base + 16 + k
793
  in_addr[c[k]] = base + 24 + k
794
+ rg, addr, _ = compile_to_reflect(cfg,net, in_addr, r + [leq] + pcm, base + 32)
795
  nl = encode_netlist(cfg, rg)
796
  print(f" compiled datapath netlist: {len(rg)} gates; interpreting all "
797
  f"65,536 (M[A], M[B]) pairs")
 
830
  return good
831
 
832
 
833
+ # =============================================================================
834
+ # a complete stored machine (SUBLEQ, 32-byte memory) hosted in the interpreter
835
+ # =============================================================================
836
+
837
+ def _subtract_net():
838
+ net = Net()
839
+ x = [f"x{k}" for k in range(8)]; y = [f"y{k}" for k in range(8)]
840
+ nx = [net.NOT(f"nx{k}", x[k]) for k in range(8)]
841
+ carry = "#1"; r = []
842
+ for k in range(8):
843
+ s_, carry = net.FA(f"sb{k}", y[k], nx[k], carry)
844
+ r.append(s_)
845
+ ot = r[0]
846
+ for k in range(1, 8):
847
+ ot = net.OR(f"zo{k}", [ot, r[k]])
848
+ leq = net.OR("leq", [r[7], net.NOT("z", ot)])
849
+ return net, x, y, r, leq
850
+
851
+
852
+ def _incr_net(): # 5-bit address arithmetic (mod 32)
853
+ net = Net()
854
+ pc = [f"p{k}" for k in range(5)]
855
+
856
+ def addc(tag, cst):
857
+ carry = "#0"; out = []
858
+ for k in range(5):
859
+ s_, carry = net.FA(f"{tag}{k}", pc[k], "#1" if (cst >> k) & 1 else "#0", carry)
860
+ out.append(s_)
861
+ return out
862
+ return net, pc, addc("i1", 1), addc("i2", 2), addc("i3", 3)
863
+
864
+
865
+ def _branch_net():
866
+ net = Net()
867
+ c = [f"c{k}" for k in range(5)]; q = [f"q{k}" for k in range(5)]
868
+ npc = [net.MUX(f"nb{k}", "leq", c[k], q[k]) for k in range(5)]
869
+ halt = npc[0]
870
+ for k in range(1, 5):
871
+ halt = net.AND(f"hb{k}", [halt, npc[k]])
872
+ return net, c, q, npc, halt
873
+
874
+
875
+ MEMB = 32
876
+ MEM = 416
877
+
878
+
879
+ def build_subleq_machine(cfg):
880
+ """A stored microprogram implementing one SUBLEQ instruction per sweep over
881
+ a 32-byte memory held in signals [MEM, MEM+8*32). Data-dependent memory
882
+ access is the interpreter's own indexed addressing: set PTR to a byte
883
+ address, then read/write the eight bit-planes at MEM + b*32 + PTR."""
884
+ WB = cfg.WORK_BASE
885
+ reg = lambda o: [WB + o + k for k in range(8)]
886
+ PC, A_, B_, C_ = reg(0), reg(8), reg(16), reg(24)
887
+ X, Y, Rr = reg(32), reg(40), reg(48)
888
+ P1, P2, P3, NPC = reg(56), reg(64), reg(72), reg(80)
889
+ leqs = WB + 88
890
+ workc = WB + 96
891
+
892
+ def zero_hi(): # PTR high bits = 0 (once)
893
+ return [([], -1, (cfg.PTR_BASE + (cfg.A - 1 - k), 0)) for k in range(5, cfg.A)]
894
+
895
+ def ptr_set(r8): # PTR low 5 = addr; high stays 0
896
+ return [([(r8[k], 1, 0)], -1, (cfg.PTR_BASE + (cfg.A - 1 - k), 0)) for k in range(5)]
897
+
898
+ def read_byte(dst):
899
+ return [([(MEM + b * MEMB, 1, 1)], -1, (dst[b], 0)) for b in range(8)]
900
+
901
+ def write_byte(src):
902
+ return [([(src[b], 1, 0)], -1, (MEM + b * MEMB, 1)) for b in range(8)]
903
+
904
+ inet, pin, p1, p2, p3 = _incr_net()
905
+ irec, _, workc = compile_to_reflect(cfg, inet, {pin[k]: PC[k] for k in range(5)}, [],
906
+ workc, fixed={**{p1[k]: P1[k] for k in range(5)},
907
+ **{p2[k]: P2[k] for k in range(5)},
908
+ **{p3[k]: P3[k] for k in range(5)}})
909
+ snet, sx, sy, sr, sleq = _subtract_net()
910
+ srec, _, workc = compile_to_reflect(
911
+ cfg, snet, {**{sx[k]: X[k] for k in range(8)}, **{sy[k]: Y[k] for k in range(8)}},
912
+ [], workc, fixed={**{sr[k]: Rr[k] for k in range(8)}, sleq: leqs})
913
+ bnet, bc, bq, bnpc, bhalt = _branch_net()
914
+ brec, _, workc = compile_to_reflect(
915
+ cfg, bnet, {**{bc[k]: C_[k] for k in range(5)}, **{bq[k]: P3[k] for k in range(5)}, "leq": leqs},
916
+ [], workc, fixed={**{bnpc[k]: NPC[k] for k in range(5)}, bhalt: cfg.HALT_SIG})
917
+ assert workc < MEM, "compiled internals collide with memory"
918
+
919
+ prog = zero_hi() + list(irec)
920
+ prog += ptr_set(PC) + read_byte(A_)
921
+ prog += ptr_set(P1) + read_byte(B_)
922
+ prog += ptr_set(P2) + read_byte(C_)
923
+ prog += ptr_set(A_) + read_byte(X)
924
+ prog += ptr_set(B_) + read_byte(Y)
925
+ prog += srec
926
+ prog += ptr_set(B_) + write_byte(Rr)
927
+ prog += brec
928
+ prog += [([(NPC[k], 1, 0)], -1, (PC[k], 0)) for k in range(5)] # PC = NPC (low 5)
929
+ return prog, {"PC": PC}
930
+
931
+
932
+ def subleq32_step(mem, pc):
933
+ A, B, C = mem[pc], mem[(pc + 1) & 31], mem[(pc + 2) & 31]
934
+ x, y = mem[A & 31], mem[B & 31]
935
+ r = (y - x) & 0xFF
936
+ m2 = list(mem); m2[B & 31] = r
937
+ npc = (C & 31) if (r == 0 or r >= 128) else (pc + 3) & 31
938
+ return m2, npc
939
+
940
+
941
+ def verify_hosted(device):
942
+ cfg = Cfg(A=14, G=296, banks=1, self_mod=False)
943
+ prog, lay = build_subleq_machine(cfg)
944
+ print(f"\nHosted machine: SUBLEQ with a 32-byte memory, one instruction per "
945
+ f"sweep\n microprogram: {len(prog)} gates; interpreter U:", end=" ")
946
+ inet, ii, io = build_net(cfg)
947
+ print(f"{len(inet.gates):,} gates (addressed span {cfg.span})")
948
+ lev = Leveled(inet, ii, io, device=device)
949
+ nl = encode_netlist(cfg, prog)
950
+ sig0 = [0] * cfg.S
951
+ sig0[cfg.NET0:cfg.NET0 + len(nl)] = nl
952
+ base = state_to_vec(cfg, {"sig": sig0, "gp": 0, "halt": 0}).to(device)
953
+ PC = lay["PC"]
954
+
955
+ def set_mem(V, mems):
956
+ for i in range(32):
957
+ for b in range(8):
958
+ V[:, MEM + b * MEMB + i] = ((mems[:, i] >> b) & 1).float().to(device)
959
+
960
+ def get_mem(Vc):
961
+ return sum(Vc[:, MEM + b * MEMB: MEM + b * MEMB + 32].long() << b for b in range(8))
962
+
963
+ # one instruction vs reference over random (memory, PC) states, batched
964
+ gen = torch.Generator().manual_seed(1)
965
+ B = 4096
966
+ mems = torch.randint(0, 256, (B, 32), generator=gen)
967
+ pcs = torch.randint(0, 30, (B,), generator=gen) # avoid the halt cell
968
+ V = base.unsqueeze(0).repeat(B, 1)
969
+ set_mem(V, mems)
970
+ for k in range(8):
971
+ V[:, PC[k]] = ((pcs >> k) & 1).float().to(device)
972
+ for _ in range(cfg.G):
973
+ V = lev.step(V)
974
+ Vc = V.cpu()
975
+ got_mem = get_mem(Vc)
976
+ got_pc = sum(Vc[:, PC[k]].long() << k for k in range(8)) & 31
977
+ exp_mem = torch.zeros_like(got_mem); exp_pc = torch.zeros_like(got_pc)
978
+ for i in range(B):
979
+ m2, np_ = subleq32_step(mems[i].tolist(), int(pcs[i]))
980
+ exp_mem[i] = torch.tensor(m2); exp_pc[i] = np_
981
+ mask = exp_pc != 31 # halting instr freezes PC
982
+ ok1 = bool((got_mem == exp_mem).all()) and bool((got_pc[mask] == exp_pc[mask]).all())
983
+ print(f" {'OK ' if ok1 else 'FAIL'} one instruction exact for {B} random "
984
+ f"(memory, PC) states (result, writeback, and branch)")
985
+
986
+ # run a stored program to a halt: count M[21] (=3) down to 0 through a loop
987
+ prog_mem = [0] * 32
988
+ prog_mem[0:3] = [20, 21, 6] # loop: subleq one, n, done
989
+ prog_mem[3:6] = [22, 22, 0] # subleq z, z, 0 (jump back to loop)
990
+ prog_mem[6:9] = [31, 31, 31] # done: branch to the halt cell
991
+ prog_mem[20], prog_mem[21], prog_mem[22] = 1, 3, 0
992
+ V = base.unsqueeze(0).repeat(1, 1)
993
+ set_mem(V, torch.tensor(prog_mem).unsqueeze(0))
994
+ halted = False
995
+ steps = 0
996
+ for _ in range(24):
997
+ for _ in range(cfg.G):
998
+ V = lev.step(V)
999
+ steps += 1
1000
+ if int(V[0, cfg.S + cfg.GPW]) == 1: # interpreter halt latched
1001
+ halted = True
1002
+ break
1003
+ got = get_mem(V.cpu())[0].tolist()
1004
+ ref_mem, ref_pc = list(prog_mem), 0
1005
+ while ref_pc != 31:
1006
+ ref_mem, ref_pc = subleq32_step(ref_mem, ref_pc)
1007
+ ok2 = halted and got == ref_mem and got[21] == 0
1008
+ print(f" {'OK ' if ok2 else 'FAIL'} stored countdown program ran to a halt in "
1009
+ f"{steps} instructions; final memory matches the emulator (M[21] = {got[21]})")
1010
+
1011
+ good = ok1 and ok2
1012
+ print("HOSTED MACHINE:", "PASS" if good else "FAIL")
1013
+ return good
1014
+
1015
+
1016
  # =============================================================================
1017
  # analog: matrix == gate + noise / conductance-mismatch margins
1018
  # =============================================================================
 
1102
  rc |= 0 if verify(cfg, args.device) else 1
1103
  rc |= 0 if verify_scale(args.device) else 1
1104
  rc |= 0 if verify_subleq(args.device) else 1
1105
+ rc |= 0 if verify_hosted(args.device) else 1
1106
  if args.cmd in ("analog", "all"):
1107
  rc |= 0 if analog(cfg, args.device) else 1
1108
  return rc