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.