[TUHS] forth on early unix
Dan Cross via TUHS
tuhs at tuhs.org
Mon Sep 22 23:43:55 AEST 2025
On Sun, Sep 21, 2025 at 11:09 PM Will Senn via TUHS <tuhs at tuhs.org> wrote:
> I'm doing forth exploring and am curious if there were forthen (is that
> even a word?) on early versions of unix that folks remember using?
Define "early versions"? I don't know about research Unix on the
PDP-11, but as Larry mentioned, Sun's boot PROM monitors used forth,
at least on SPARC (did they on 68k as well?). Some of the
functionality was pretty neat: see the section titled "Symbiosis" in
this short article from Mike Shapiro, which is nominally on little
languages, but comes by way of describing the genesis of the Solaris
MDB debugger: https://queue.acm.org/detail.cfm?id=1508217 The idea of
Forth commands named for assembler mnemonics that assembled into SPARC
instructions seems particularly inspired.
These ideas still resonate and have relevance.
As some folks may know, Oxide hardware doesn't use a BIOS or UEFI or
any such thing; we boot almost directly into Unix. I say "almost"
because we have a (very) thin loader stored in flash that runs
directly from the reset vector and that initializes the x86 BSP, loads
the kernel nucleus into RAM, and jumps to its entry point. This lets
us treat the kernel like a standard, run-of-the-mill ELF binary:
https://github.com/oxidecomputer/phbl/
However, that's not what we use when we're doing bringup for a new
board or CPU microarchitecture: the cost, and wear on the part, of
rewriting flash for each iteration is too high. For that, we have a
separate program that runs from the reset vector and is kinda-sorta a
"debugger" (not precisely a debugger in the "set a breakpoint and
continue" sense, but rather in the, "can inspect a bunch of system
state" sense). That program has a REPL that a user can interact with
over a serial port, and supports sending a binary over the UART that
is then loaded and run.
The REPL has to be relatively easy to use, as we have non-kernel folks
using it (EEs, for example). A kind of neat idea it implements is
support for something analogous to pipelines: one can chain various
commands together so that the output of one becomes input to the next,
though I think in context it's closer to functional composition than
pipes per se: suppose y = g(x) and I want to run f(y); in Algebra,
this is the same as f(g(x)), and in our program, can be written as
either `f . g x` (vaguely inspired by Haskell) or `g x | f` at the
REPL. For details, see https://github.com/oxidecomputer/bldb/
However, when I was writing that, I was struggling a bit with how to
organize the internal representation for commands and their arguments,
and how to chain them together: if I gave an argument to a command,
how would that interact with a chain? Would the explicit argument
come first, last, what? How would the command know to read arguments
from the pipe state, or whatever one would call it, versus what was
given explicitly to the command? For that matter, how would the pipe
state be represented? What if a command outputs more than one data
(say, an address and a length)? Would I fill in some per-command
(these are all builtins) data structure and pad it out to the expected
number of elements by reading from some vector of prior-command data?
What if the sizes didn't match up? What if I need to stash some data
to pass to something later? It was getting messy.
I was discussing this with Bryan Cantrill and he suggested I read
Shapiro's article. The solution, which was so obvious, hit me once I
got to the section about Forth and the Sun PROM monitor: use a stack
to pass state across commands. A command takes its arguments from the
stack, pulling only as many elements as it needs, and pushes its
output back onto the same stack. If the REPL reads a command of the
form, `foo a b` it pushes `b`, then `a`, and then invokes `foo`. A
chain of commands is represented by a separate stack. I added
commands to duplicate the item at the top of the stack, swap the top
two elements, and of course push and pop; all very Forth-inspired.
The result was a beautiful simplification: the system is easy to use
but surprisingly powerful.
Note that this also meant I could refer to data put on the stack by
one command much later; so for example, I can read compressed data
over the serial port, decompress it (returning the base address and
size of the decompressed data), duplicate the data returned from
decompression, use the duplicate to "mount" a ramdisk filesystem image
leaving the original on the stack, then load a file from the ramdisk
(which pushes its entry point onto the stack), and call its entry
point, passing the previously-duplicated decomp data as arguments
(indeed, that's exactly how we boot development images on the system).
I suspect this is the sort of thing folks did in Forth regularly, and
it continues to give inspiration in 2025!
- Dan C.
More information about the TUHS
mailing list