#!/usr/bin/env python3 # ILDEC: a minimalistic DEC PDP-8/L emulation core in (Micro)Python # See README.md for the usage guide # Created by Luxferre in 2026, released into the public domain import sys try: import select except ImportError: select = None from array import array try: # T-DeckARD workaround from deck.input import input from repl.tdeck_repl import Pydos_ui def default_keyboard_check(pdp8): if pdp8.kbd_delay <= 0: pdp8.kbd_delay = 1000 if Pydos_ui.serial_bytes_available(): c = Pydos_ui.read_keyboard(1) b = ord(c) if c else None if b == 4 or c == "": pdp8.halted = True return if b is not None: pdp8.kbb, pdp8.kbf, pdp8.kbd_delay = (13 if b == 10 else b) | 0x80, 1, 500 except: def default_keyboard_check(pdp8): try: if pdp8.kbd_delay <= 0: pdp8.kbd_delay = 1000 try: import supervisor has_bytes = supervisor.runtime.serial_bytes_available except (ImportError, AttributeError): has_bytes = pdp8.poller.poll(0) if pdp8.poller else (select.select([sys.stdin], [], [], 0)[0] if select else False) if has_bytes: try: import os; b, c = os.read(0, 1)[0], None except Exception: c = sys.stdin.read(1) b = ord(c) if c else None if b == 4 or c == "": pdp8.halted = True return if b is not None: pdp8.kbb, pdp8.kbf, pdp8.kbd_delay = (13 if b == 10 else b) | 0x80, 1, 500 except Exception: pass def set_cbreak(enable): try: import termios fd = sys.stdin.fileno() if enable: import tty; tty.setcbreak(fd) else: attr = termios.tcgetattr(fd) attr[3] |= (termios.ICANON | termios.ECHO) termios.tcsetattr(fd, termios.TCSADRAIN, attr) except Exception: pass def char_output(c): sys.stdout.write(chr(c & 0x7F)) if hasattr(sys.stdout, 'flush'): sys.stdout.flush() class ILDEC_PDP8: def __init__(self, init_pc=0, init_sr=0, keyboard_check = None, outchar_cb = None): self.memory = array('H', [0] * 4096) self.kbdcheck, self.outc = keyboard_check or default_keyboard_check, outchar_cb or char_output self.reset(init_pc, init_sr) self.poller = None try: import supervisor except ImportError: try: if select: self.poller = select.poll() self.poller.register(sys.stdin, select.POLLIN) except Exception: pass def reset(self, pcval=0, srval=0): self.link = self.ac = self.kbb = self.kbf = self.chb = self.chf = self.kbd_delay = 0 self.ie = self.ion_delay = 0 self.pc, self.sr, self.halted = pcval, srval, False def start(self, start_addr=0o200): self.pc, self.halted = start_addr, False set_cbreak(True) try: while not self.halted: self.step() finally: set_cbreak(False) def rotate_ac_link(self, dir, times): for _ in range(times): if dir > 0: self.ac, self.link = (self.ac >> 1) | (self.link << 11), self.ac & 1 else: self.ac, self.link = ((self.ac << 1) | self.link) & 0xFFF, self.ac >> 11 def step(self): if self.halted: return if not self.kbf: self.kbdcheck(self) if self.halted: return if self.kbd_delay > 0: self.kbd_delay -= 1 if self.ion_delay > 0: self.ion_delay -= 1 if self.ion_delay == 0: self.ie = 1 if self.ie and (self.kbf or self.chf): self.ie, self.memory[0], self.pc = 0, self.pc, 1 instruction, current_pc = self.memory[self.pc], self.pc self.pc = (current_pc + 1) & 0xFFF opcode, rest = (instruction >> 9) & 7, instruction & 511 if opcode < 6: target = (current_pc & 0xF80 if rest & 0x80 else 0) | (rest & 0x7F) if rest & 0x100: if 8 <= target <= 15: self.memory[target] = (self.memory[target] + 1) & 0xFFF target = self.memory[target] if opcode == 0: self.ac &= self.memory[target] elif opcode == 1: self.ac, self.link = (self.ac + self.memory[target]) & 0xFFF, self.link ^ ((self.ac + self.memory[target]) >> 12) elif opcode == 2: self.memory[target] = (self.memory[target] + 1) & 0xFFF if not self.memory[target]: self.pc = (self.pc + 1) & 0xFFF elif opcode == 3: self.memory[target], self.ac = self.ac, 0 elif opcode == 4: self.memory[target], self.pc = self.pc, (target + 1) & 0xFFF elif opcode == 5: self.pc = target & 0xFFF elif opcode == 6: self.execute_iot(rest) elif opcode == 7: self.execute_opr(rest) def execute_opr(self, opr): if opr in (0, 0o401): return group = 1 if not (opr & 0x100) else (3 if (opr & 1) else 2) b10 = (opr >> 1) & 1 if group == 1: if opr & 0x80: self.ac = 0 if opr & 0x40: self.link = 0 if opr & 0x20: self.ac ^= 0xFFF if opr & 0x10: self.link ^= 1 if opr & 1: self.ac, self.link = (self.ac + 1) & 0xFFF, self.link ^ ((self.ac + 1) >> 12) if opr & 0x08: self.rotate_ac_link(1, b10 + 1) if opr & 0x04: self.rotate_ac_link(-1, b10 + 1) if b10 and not (opr & 0x0C): self.ac = ((self.ac << 6) | (self.ac >> 6)) & 0xFFF elif group == 2: if bool(((opr & 0x40) and self.ac > 0x7FF) or ((opr & 0x20) and not self.ac) or ((opr & 0x10) and self.link)) != bool(opr & 0x08): self.pc = (self.pc + 1) & 0xFFF if opr & 0x80: self.ac = 0 if opr & 0x04: self.ac |= self.sr if b10: self.halted = True def execute_iot(self, cmd): dev, op = (cmd >> 3) & 63, cmd & 7 if dev == 0: if op == 1: self.ion_delay = 2 elif op == 2: self.ie = self.ion_delay = 0 elif op == 3 and (self.kbf or self.chf): self.pc = (self.pc + 1) & 0xFFF elif op == 4: self.ac = (self.link << 11) | ((1 if (self.kbf or self.chf) else 0) << 9) | (self.ie << 7) elif op == 5: self.link, self.ion_delay = (self.ac >> 11) & 1, 2 elif op == 7: self.ac = self.link = self.kbf = self.kbb = self.chf = self.chb = self.ie = self.ion_delay = 0 elif dev == 3 and op != 5: if (op & 1) and self.kbf: self.pc = (self.pc + 1) & 0xFFF if op & 2: self.ac = self.kbf = 0 if op & 4: self.ac |= self.kbb elif dev == 4 and op != 5: if op == 0: self.chf = 1 else: if (op & 1) and self.chf: self.pc = (self.pc + 1) & 0xFFF if op & 2: self.chf = 0 if op & 4: self.chb, self.chf = self.ac, 1; self.outc(self.ac) def load_oct(self, filepath): with open(filepath, 'r') as f: for line in f: try: p = line.split(':') self.memory[int(p[0], 8) & 0xFFF] = int(p[1].strip().split()[0], 8) except Exception: pass # entry point (for ildec.py to be usable as a module) def ildec(fname='', init_pc=0o200, init_sr=0): print('\n ===== ILDEC PDP-8/L ONLINE =====') emu = ILDEC_PDP8(init_pc, init_sr) if fname: emu.load_oct(fname) while True: print(f"\nPC: {emu.pc:04o} | L AC: {emu.link} {emu.ac:04o} | SR: {emu.sr:04o}\n") cmd = input('> ').strip().lower() if cmd == 'q': break elif cmd == 'r': emu.reset(init_pc, init_sr) elif cmd == 'n': emu.__init__(init_pc, init_sr), print('All memory cleared!') elif cmd == 'c': emu.start(emu.pc) elif cmd == 'l': emu.pc = emu.sr elif cmd == 's': emu.step() elif cmd in ('e', 'd'): if cmd == 'd': emu.memory[emu.pc] = emu.sr print(f"{emu.pc:04o}: {emu.memory[emu.pc]:04o}") emu.pc = (emu.pc + 1) & 0xFFF elif cmd == 'i': fname = input('File path: ').strip() if fname: emu.load_oct(fname); print(f'Memory imported from {fname}') elif cmd == 'x': fname = input('File path: ').strip() if fname: with open(fname, 'w') as f: for i, val in enumerate(emu.memory): if val: f.write(f"{i:04o}: {val:04o}\n") print(f'Memory exported to {fname}') else: try: emu.sr = int(cmd, 8) except Exception: print('Invalid action!') # Run it standalone if __name__ == '__main__': a = sys.argv if hasattr(sys, 'argv') else [] ildec(a[1] if len(a) > 1 else '', int(a[2], 8) if len(a) > 2 else 0o200, int(a[3], 8) if len(a) > 3 else 0)