Writing Something Real in OctoGo: A PDP-11/40 That Boots Unix V6

The OctoGo post in July ended with a question: whether the zero-allocation constraints feel workable once you try to write something real. This post is the something real.

p2-11 is a PDP-11/40 emulator for the Propeller 2, written in OctoGo. It boots Unix V6 and RT-11 from RK05 disk packs kept as files on the SD card of a P2 Edge. V6 logs in, and compiles and runs C with its own compiler. Here it is doing that, recorded on the board at real speed:

Unix V6 booting on the board and compiling hello, world, at real speed

That is 95 seconds from ogo run to the last sync. The first twelve are the build, the load, and the emulator timing itself; then V6 boots, root logs in, and cc takes about half a minute over hello, world.

How it was built

Plainly: with AI help, and a lot of it. Claude, Anthropic’s model, working in Claude Code sessions, wrote the emulator, its tests, its scripts and the project’s notes. 57 of the repository’s 58 commits carry its Co-Authored-By line; the one that does not is the initial commit, an empty README. My part was the direction and the calls: what to build and in which order, what counts as done, which disk images may be used and which may never enter the repository, when a thing is committed and pushed. I also carried the SD card to the card reader when a pack had to go onto it.

The OctoGo compiler has a Claude session of its own. The two sessions work side by side, and what p2-11 found in the compiler went across, through me or directly, as a small program that shows it, measured on the board, and came back as a release. Twice, another AI agent reviewed the whole project from the outside, and both times it found faults the tests had missed.

It took four days: an empty repository on 27 September, Unix V6 compiling C on the 29th, and this post on the 30th. That is the part I am least sure how to feel about, so let me say what I think made the help usable rather than merely fast, because it was not the typing:

  • An oracle. SimH’s PDP-11/40 decides what is right, not the model’s confidence and not mine. Every instruction case, every disk operation and every session with an operating system is compared with what SimH does.
  • The board is the verdict. Nothing about what a binary does is written down before it was measured on the board, with the compiler’s version and the clock beside the number.
  • Reviews are reproduced first. What a reviewing agent claims becomes a failing test before anyone touches the code, and the test stays.
  • The notes are the memory. A session starts knowing nothing. The project’s CLAUDE.md, over 9,000 words by now, says what exists, what was measured and when, what was decided and why, and it is kept true as the code moves.

The machine

  • the KD11-A processor with the extended instruction set (MUL, DIV, ASH, ASHC)
  • the KT11-D memory management, with 248 KB of memory
  • a DL11 console, which is the serial line the program was loaded through, so the loader’s terminal is the PDP-11’s terminal
  • a KW11-L line clock
  • an RK11 disk controller with up to eight RK05 drives, each pack a file on the card’s FAT32 volume

That is 4,165 lines of OctoGo, 2,960 of tests, 530 of MACRO-11 for the PDP-11 programs it carries, and 2,132 of scripts.

What a real program did to the language

Most of what p2-11 taught OctoGo was about what a call costs.

The first version of the processor was written the way the same program would be in Go: an instruction went through Step, execute, a function for its group, one for the instruction, and from there to operand, load, readWord, aborted, nz and store. It ran 21,059 PDP-11 instructions a second. With one call to an instruction where that can be had, an operand in a register dealt with where it is met, and no helper on the way that only tests something, it ran 111,316, with the same compiler. Same instructions, same order, 5.3 times as fast.

On this target a call and its return cost 75 to 115 clocks, what forty or fifty instructions do, and the C backend inlined nothing with a branch in it. A helper of two tests and three returns took 83 clocks a call. In the checked build it was worse: the nil check of a receiver and the bounds check of an index were themselves calls, so a method that tested a field of its receiver took 211 clocks where the unchecked build took 28.

v0.46.0 of OctoGo answers that: it inlines a small function itself, a branch or a check in it or not, and those two now take 29 and 51 clocks. For p2-11 it is worth 0.1%, because p2-11 had already been written around the cost by hand. Which is the honest summary: the language got better, and the program that found out why still reads like it was written for the old compiler.

Other things the emulator ran into, each now fixed:

  • A named constant was an object in the generated C, read from hub RAM at every use. Making it its value made the emulator 6% faster checked and 8% unchecked.
  • Assigning a call’s several results to fields took the compiler 23 seconds in a program of twelve lines, and all the memory the machine had in a larger one.
  • An array literal was one line of C, and a table of 8000 numbers was more than the backend’s preprocessor takes in a line.
  • An unsigned 32-bit number ordered against an untyped named constant was compared as if it had a sign. The backend warned, and built, and on the board b-a < patience was true for a, b = 12000, 5000 and const patience = 10000. Since then a build of p2-11 says nothing, and one that warns has found something.
  • if two(); ok { was a syntax error. Go allows any simple statement there.

Five cogs and no locks

main steps the machine. One cog does nothing but read the serial line, since nothing is buffered behind a byte read and a byte arriving while the cog is elsewhere is lost. One writes it. One counts the cycles of the power line for the clock. One opens the SD card and moves blocks between the card and the PDP-11’s memory while the program runs.

Between them there is no lock and no channel. Every variable two cogs share is written by one of them only: the console has a ring whose head the reading cog writes and whose tail the machine’s cog writes; the disk has an order with its number, which the machine’s cog writes, and a report with the number of the order it is of, which the disk’s cog writes. Memory is the one exception, as it is on a bus that a device can take. A channel’s rendezvous would have stalled the cog that must never stall, the one reading the line.

The oracle

The processor is held to SimH’s 11/40 by 2902 cases, each a machine before and after one or two steps, the after being what SimH made of it: every instruction and addressing mode, the traps, and for the memory management every way an access can abort. A script has SimH make them, and the same table runs on the board and on the PC.

On the PC it runs in the twin. OctoGo is close enough to Go that the packages of the emulator which do not touch the P2 are Go once they get a package clause, so a script copies them, adds one, and runs their tests with the Go toolchain, built for 386 so that int stays 32 bits. That answers in a second and runs things the board has no time or room for. It is not the verdict; the board is. That was July’s lesson, and it held.

Above the instructions are sessions with the operating systems: a script boots RT-11 on the board and in SimH, has each copy a file, compare and delete it, and compares what the two consoles said, byte for byte, and what the two packs then hold. The one with Unix V6 logs in, compiles a C program and runs it, and the board says the same 1357 bytes SimH says.

What went wrong

Of the differences from SimH met on the way, one was the machine’s: it counted an instruction it could not fetch as a step. The rest were the tests’, which is why a failing test is suspected first here.

The reviews found what the tests did not, and in the same place both times: not in any instruction, but in combinations of events. A reset with a byte on its way to the transmitting cog. A trap’s second push after its first had failed. An interrupt let in by the one before it. A page of memory that goes past the end of the 18-bit bus and should go on at zero. RESET executed with the memory management on. The console on a machine of 64K words or more, where a length narrowed to 16 bits became zero. A FAT32 volume whose two tables are not mirrors of each other, where reading the wrong one could have written a pack over another file. Each has its test now, and each test failed before the fix.

A few of the others:

RT-11 took a line the wrong way round. A command sent all at once reached it in the wrong order. RT-11’s console driver turns its receiver interrupt back on after about ten instructions and counts on the next character taking a character’s time to arrive. At 230400 baud a character takes five instructions. The console now hands the program what arrived no closer together than 1000 instructions apart, which is what SimH does.

The compiler made the SD card fail. The card is driven pin by pin, and a loop that read a bit too soon after the clock fell worked until a compiler made it faster. Every block is checked against the card’s checksum, which is how it was found. The wait is in the source now with what it was measured to need: with 0, 1 or 2 the first long transfer fails, with 3 to 6 all of 500 blocks read. It is 8.

Unix V6’s disk was broken in 1994. The root pack of SimH’s V6 kit has block 654 three times in its free list, so files written after it boots share a block, which is how the C compiler’s second pass read what its first had not written. icheck -s rebuilds the list. The subtle part is that the kernel still holds the old list in memory afterwards, and the kit’s /etc/rc frees block 654 once more at every boot, since it is /etc/mtab’s. Write one file before the machine stops and the next sync puts the broken list back. V6’s own manual page says to reboot at once. The pack also had a temporary file of the compiler’s in /tmp, dated 20 August 1994.

The terminal was full of garbage. The first time I logged in on the board by hand, yesterday, the screen filled with replacement characters and every line started where the previous one ended. V6 sends each character with its even parity in the eighth bit, so login: leaves the machine as lo\xe7i\xee: and a carriage return as 0x8d, which no terminal of today takes for one. SimH’s console strips the eighth bit unless told otherwise. The emulator did not, and the tests never saw it, because the scripts that compare the board with SimH were stripping it too. Now the program sends seven bits, and the script compares the bytes as they come.

Numbers

On a P2-EC, ogo v0.46.0:

  • about 105,000 PDP-11 instructions a second in the default checked build at 160 MHz, 118,000 built --unchecked, and 147,000 unchecked at 200 MHz
  • the memory management costs a tenth of that with the unit off, and an eighth more with it on
  • the session with Unix V6, from the load to the last prompt, about 95 seconds
  • a block of the SD card read in about 1.6 ms, checked

Fifty years apart

The same computer, fifty years apart: a PDP-11/40 with two RK05 drives at the U.S. Army’s Natick labs in 1976, and a P2 Edge in a paper box on my desk in 2026. The new one runs Unix at a quarter of the speed, for a seven-thousandth of the price. (A thousandth, if you forget fifty years of inflation.)

The prices are from DEC’s PDP-11/10 and 11/40 price list of 1 January 1973: the 11/40 with 8K words of core and a Teletype $12,995, the extended instruction set $1,200, memory management $2,300, the line clock $250, core to about the 248 KB p2-11 gives its machine $63,100, the RK11 $5,900, and two RK05 drives at $5,100 with a $99 pack each. That is about $96,000, some $700,000 today, before the cabinets all that memory needs. The P2 side is about $100: the module, a breakout board, a Prop Plug and a card. The speed is the benchmark’s: by the timing appendix of the PDP-11/40 Processor Handbook an 11/40 with memory management runs it at about 444,000 instructions a second, and the P2 at 104,000. The photo on the left is by the U.S. Army Natick Soldier Systems Center, 13 July 1976, public domain, via Wikimedia Commons.

What is next

A wish rather than a promise: a PDP-11 that needs no PC, booting from the Edge’s flash, with a screen and a keyboard of its own. The screen would be a text console on the P2’s video hardware, written as an OctoGo package of its own, since video is what OctoGo has nothing for yet. Eric Smith’s ANSI text driver for the P2 looks like a good place to start, and Marco Maccaferri has MIT-licensed USB and PS/2 keyboard drivers for the P2, the USB one after garryj’s.

Questions I would like answers to:

  • Have you run it? On which board, with which card?
  • What would you boot on it next? Anything that fits an 11/40 with 248 KB and RK05 packs, and that may be passed around.
  • If you have used AI help on a project like this, what kept it honest for you? An oracle did it here, and I am not sure it would have gone as well without one.

The source is at https://gitlab.com/cznic/p2-11, mirrored at https://github.com/modernc-org/p2-11. The OctoGo release it needs is https://github.com/modernc-org/ogo/releases/tag/v0.46.0, binaries for Linux, Windows and macOS, no Go needed. The forum thread is https://forums.parallax.com/discussion/178200/.

Comments

Popular posts from this blog

Taming the Flat AST: Ergonomics in the Age of Zero Allocations

Building a High-Performance JSON Parser in Go with Egg

Building OctoGo: A Go-like Language Where Goroutines Are Cores