Here are some PDP-8 programming techniques that PDP-8 programmers will know and ambitious readers examining my source code may notice.
Self-Modifying Code
Very early stored program machines often relied on self-modifying code for fundamental operations. Consider, for example, the early 1950s IBM 701, one of the first commercial computers, with a vacuum tube CPU and, at first, Williams tube memory. The 701 was an 18-bit machine with a 12-bit direct address in the instruction, and no other way to generate an address. Using an address computed at run time, however, is a necessary capability of stored program machines. For runtime addressing it had the STORE ADDRESS instruction, which would store the low 12 bits of the accumulator in the low 12 bits of an instruction, leaving alone the high 6 bits, which contained an op code. Then the modified instruction would be executed.
By the 1960s index registers and indirect addressing mostly did away with that trick, but the PDP-8 retained the need for self-modifying code for extended addressing and other purposes. To access memory in a field other than the current instruction field (CIF, the one containing the instructions being executed) you’d execute a CDF (change data field) instruction, which contained the 3-bit field address in the instruction itself. If you knew the desired field at assembly time you were all set. But if the field was computed at runtime you’d need to create a CDF instruction for subsequent execution.
A good example in my source code is the supervisor call (SVC) dispatcher at address 0326. The SVC instruction could be used in any field, and the SVC dispatcher, and the handler it dispatched to, would need to pick up arguments in that field. So the dispatcher modified the subroutine SVCDF at 0354 to do nothing more than just execute a CDF to the field that executed the SVC. The dispatcher (at 0336) and handler (e.g. FORK at 1255) could then call SVCDF as needed.
Another important use of self-modifying code involved extended arithmetic element (EAE) instructions. Multiply, divide, and shift were two-word instructions that got one of their operands from the second word. If that operand was an assembly-time constant, you were all set. If it was computed at runtime, you’d have to modify the instruction.
Argument Passing on a Single-Accumulator Machine
The PDP-8 was a triumph of simplicity and elegance. It was a single-accumulator machine with no instruction to load the accumulator from memory, and amazingly you didn’t really need one. Instead you used TAD (twos complement add, memory to AC) to load the accumulator, which worked because it was easy to keep AC at zero just before needing to load it. (It was called twos complement add because after years of ones-complement designs, people finally realized that von Neumann was right all along).
By convention, subroutines were called, and returned, with AC = 0 unless there was an argument or return value in AC. With one accumulator and no modern stack, a common convention for subroutines needing multiple arguments was to place them in memory just after the JMS that called the subroutine. This worked cleanly with JMS (jump subroutine), ISZ (increment memory and skip if zero), and indirect addressing.
This style can be seen in CHOINT at 0662, which takes only one argument but expects it in the word after the JMS, rather than the AC, because the argument is always an assembly-time constant. JMS stores the return address at 0662 and jumps to 0663. Since the return address is the address of the argument, it is fetched at 0663 with TAD I CHOINT. Then ISZ CHOINT at 0664 increments the address to skip the argument and point to the true return address. ISZ, also used for counting, never skips with this style because nobody puts a JMS at 7776. Finally CHOINT returns with JMP I CHOINT at 0636. If there were more arguments, they’d be fetched the same way. TAD, ISZ, and DCA (deposit and clear AC) work together to do much of the heavy lifting on the PDP-8. TAD and ISZ have many uses unrelated to adding and counting, part of the elegance of the instruction set.
Catch and Throw
Another common convention was that many subroutines would return to the address after the JMS if an error was detected, and skip over that address if successful. The non-skip return was in effect what we’d recognize as a throw, because the caller could choose to catch the error or just pass it along (rethrow) to its caller. To catch the error, the caller would put a JMP to an error handler following the JMS. To rethrow, it would simply put the standard JMP indirect through its return address there, causing another non-skip return. Then the caller would ISZ its return address to skip-return if successful. The non-skip returns would be passed up the call chain until someone wanted to catch the error.
A great example in the kernel starts at the SVC dispatcher at 0326. SVC skip-returns if successful, doing so with the ISZ at 0350. It calls a handler with a JMS at 0345, expecting a skip-return if the handler is successful. One such handler is FORK at 1252, which calls ALOC at 1271 to get storage for a new PCB. If ALOC fails, its non-skip return goes to 1272, which just passes the non-skip back up to SVC, which in turn passes it back to its caller.
In some circumstances the non-skip return should be logically impossible, i.e. a bug. In that case you’d put a JMS to a global fatal error handler, or maybe just a halt instruction (JMS rather than JMP so the global hander would know the address of the offending call).