Recently Written · git

forthlisp-z80

Z80 target for forthlisp: Z80 Forth kernel built by an SBCL assembler, z80pack cpmsim

git clone https://github.com/equwal/forthlisp-z80

Log | Files | Refs


NOTES.md (6253 bytes)

1 # Z80 Forth/Lisp machine
2 
3 A Scheme that runs on an emulated Z80 with 64 KiB of address space. Each layer
4 is built only from the layer below it.
5 
6 ```
7  4. LISP     prelude.scm  (Scheme: lists, numbers, let/cond/case/do macros)
8              lisp.fs      (Forth: heap, GC, reader, eval, printer, primitives)
9                  | loaded as source text over the serial console by host.lisp
10  3. FORTH    kernel.lisp  (our own indirect-threaded Forth: inner interpreter,
11                            primitives, dictionary, outer interpreter, compiler,
12                            serial console)
13                  | assembled on the host
14  2. SBCL     asm.lisp     (two-pass Z80 assembler hosted in SBCL)
15                  | writes kernel.mos (Mostek header + bytes, loads at 0000)
16  1. Z80      machine code, run by z80pack cpmsim
17 ```
18 
19 No third-party Forth is used. The Lisp is not generated by SBCL: SBCL only
20 sends `lisp.fs` and `prelude.scm` as text, line by line, and the Z80 Forth
21 compiles and evaluates them.
22 
23 ## Layer 2: why a small assembler, not sb-assem
24 
25 SBCL's `sb-assem` (`define-instruction`, segments, emitters) is the right
26 model, and Paul Khuong's breadboard post uses it. But it is wired to the native
27 backend: instruction formats, registers (TNs), fixups and the disassembler all
28 belong to the x86-64 VOP machinery, and defining Z80 instructions there means
29 working inside `SB-VM` internals of one SBCL version. A Z80 needs about 70
30 encodings. `asm.lisp` keeps the breadboard style (instructions are Lisp data,
31 emitters write into a byte vector, Lisp functions act as VOP-like templates:
32 `code`, `colon`, `flag-code` in `kernel.lisp`) in 170 lines that do not depend
33 on SBCL internals.
34 
35 Syntax: `(ld a (@ hl))`, `(ld (@ ix 3) b)`, `(jp nz loop)`, `(dw docol)`,
36 bare symbols are labels. `(@ x)` is indirection.
37 
38 ## Layer 3: the Forth kernel
39 
40 - Indirect threaded. BC = IP, SP = data stack, IX = return stack.
41 - 51 primitives in assembly, 64 colon words threaded at build time by
42   `compile-tokens` (control flow resolved like a Forth compiler would).
43 - Words: stack, memory, arithmetic (`um*`, `um/mod`, `d+`), comparisons,
44   `key emit key?`, `cmove fill s=`, `: ; if else then begin while repeat until
45   again recurse exit variable constant create ' ['] literal ( \ s" ." char
46   [char] catch throw words depth bye`.
47 - Console: z80pack cpmsim TCP console 1, ports 40 (status: bit 0 rx ready,
48   bit 1 tx ready) and 41 (data). The kernel echoes input; CR, LF or CR LF end
49   a line; backspace edits. After each line it prints ` ok`.
50 
51 ## Layer 4: the Scheme
52 
53 - One heap object = one 4-byte cell (car, cdr) + a type byte. Pairs, symbols,
54   closures, primitives, 32-bit integers, strings (list of char codes),
55   vectors (list of elements), macros.
56 - Mark-sweep GC. Roots: the symbol list plus a conservative scan of the data
57   and return stacks.
58 - Proper tail calls: `eval` is a loop; `if`, `begin`, bodies and closure
59   calls continue the loop.
60 - Special forms in Forth: `quote if define set! lambda begin define-macro`.
61   Everything else (`let let* letrec letrec* named let cond => case and or when
62   unless do`) is a `define-macro` in `prelude.scm`. An expansion replaces its
63   call form in place, so each form expands once.
64 - `(forth)` returns to the Forth prompt; `lisp` re-enters the REPL.
65 
66 ## Emulator: z80pack cpmsim
67 
68 Chosen because it is maintained (release 1.39, 2025), MIT licensed, builds
69 from source with only a C compiler, runs headless, and has a TCP console built
70 in (`conf/net_server.conf`), so `PORT` works like QEMU's `-serial tcp:`. It
71 also ships `z80asm`, the reference assembler for the layer 2 tests.
72 RunCPM needs a CP/M program and stdio only; MAME is large to build on g.
73 
74 `build-emu.sh` clones z80pack to `~/src/z80pack` (tested at 91fd28e) and builds
75 it with three edits: the named-pipe auxiliary device is off (it needs a
76 helper and is shared between instances), a stale constant in that code path
77 is fixed, and a TCP console peer that disconnects is dropped instead of
78 stopping the CPU (upstream stops with a fatal I/O error; the Lisp launcher
79 reconnects after loading).
80 
81 ## Memory map and limits
82 
83 | Range | Use |
84 |---|---|
85 | 0000-0DDC | kernel image (3549 bytes) |
86 | 0DDD-E7FF | dictionary: lisp.fs, heap, types, symbol names (`here` = B2FA after load, 13.5 KiB free) |
87 | E800-EBFF | terminal input buffer: 1023 characters per line |
88 | EC00-F7FF | return stack, 3 KiB (IX starts at F800) |
89 | F800-FFFF | data stack, 2 KiB (SP starts at 0000) |
90 
91 - Heap: 7000 cells (28000 bytes) + 7000 type bytes. 5129 cells free after the
92   prelude. `(room)` returns the free count after the last GC.
93 - Integers: 32-bit signed, boxed; overflow wraps. `quotient`, `remainder`,
94   `modulo` need a divisor in -32768..32767 (an error otherwise).
95 - `sqrt` is an integer square root (floor); `floor` is the identity. No
96   floats, rationals, characters (a char is its code), or `call/cc`.
97 - One expression may not span lines; a line holds 1023 characters.
98 - Vectors and strings are lists inside: `vector-ref` is O(n).
99 - No arity checks on closures; no stack overflow checks (deep non-tail
100   recursion over about 150 levels can overrun the 3 KiB return stack).
101 - Macros are not hygienic; expansions use the names `%t %c %k %do`.
102 - Symbols are never collected.
103 
104 ## Running
105 
106 ```
107 z80-forth             # Forth prompt
108 z80-lisp              # Forth + lisp.fs + prelude.scm, Scheme REPL
109 PORT=4500 z80-lisp    # another TCP port for the emulated console
110 ./test.sh             # all three test layers
111 ```
112 
113 ## Tests and measured results (2026-10-04)
114 
115 | Layer | Test | Result |
116 |---|---|---|
117 | 2 assembler | `test-asm.lisp`: every encoding form vs z80pack `z80asm` | 434 passed, 0 failed |
118 | 3 kernel | `host.lisp test-forth`: kernel words over serial | 45 passed, 0 failed |
119 | 4 Lisp | `host.lisp test`: `../r7rs-tests.scm` | 205 passed, 0 failed |
120 
121 `test.sh` runs all three in 41 s. cpmsim runs unthrottled at about 450 MHz
122 of emulated Z80 clock.
123 
124 ## Conformance file
125 
126 `r7rs-tests-z80.scm` is the 205-case suite this port was validated against (the shared suite as of
127 the merge). The shared suite has since grown cases for number, character and blob features
128 that only the STM32 Scheme has; the Z80 harness has no per-case timeout, so it keeps its pinned copy.