I’d like to share my experience developing system software for a retro platform (Z80).
The Problem: Synchronization Hell
It all started with the driver specifications. Initially, I implemented them using a mix of C and ASM. After completing the core drivers, the sheer scale of the task became clear: the platform's numerous modes required a massive amount of redundant manual work.
Worst of all was maintaining consistency. Any minor change to a data structure had to be manually propagated through the entire chain: .h -> .c -> .inc -> .asm. Field offsets would constantly drift, and keeping track of register assignments and method arguments became a nightmare.
The Solution: From Annotations to Metaprogramming
My first idea was to procedurally generate files from special annotations within the assembly code (which led to the XIFF specification). During development, the annotation syntax naturally evolved toward S-expressions.
That’s when I asked myself: Why use annotations at all when I can write everything in Lisp? This allows for true metaprogramming—using Lisp macros to create a flexible DSL that describes the driver logic itself.
The Experiment: An Assembler on SOOT
As an experiment, I wrote a Z80 assembler on SOOT (my dialect of Lisp). It allows the power of macros to be used directly within the program body.
Source Code Example:
Lisp
(defmacro subroutine (name &rest code)
`(list (lab ',name) ,@code))
(defparameter test
(lzasm-z80
$bla
(jmp 'blo)
(ld @a @b)
$bli
(jmp 'blo)
(ld @c (+ 1 3)) ; Compile-time math
$ble
(ld @c (+ 1 3))
(ld @c (+ 1 3))
$blu
(ld @a @c)
$blo
(subroutine blubber
(ld @a @c))
(jmp 'blo)
(jmp 'blu)))
Compilation Result (The system automatically handles jp vs jr optimization and calculates offsets):
Plaintext
0000 | bla: | 0000 | 18 0A | jr blo 0002 | 78 | ld a, b 0003 | bli: | 0003 | 18 07 | jr blo 0005 | 0E 04 | ld c, 4 0007 | ble: | 0007 | 0E 04 | ld c, 4 0009 | 0E 04 | ld c, 4 000B | blu: | 000B | 79 | ld a, c 000C | blo: | 000C | blubber: | 000C | 79 | ld a, c 000D | 18 FD | jr blo 000F | 18 FA | jr blu
I prioritized diagnostics. The system provides the exact error location in the source file, which is critical when using heavy macro generation:
Plaintext
─── ERROR ──────────────────────────────────
at common/xiff/soot/asm-test.sot:48
(jmp 'blo-undefined)
^
Error: Can't find label blo-undefined
The Next Step: Integrating the Type System
I am currently working on integrating a type system. This allows for declaring enums, bitfields, structures, and even class methods directly in the code.
Type Declaration:
Lisp
(deftype test-vector (basic)
((x int8 1)
(y int8 2))
(:methods
(new (int8 int8) object)
(set (int8 int8) none)
(len () int8)))
Now, the compiler environment has full introspection—it knows sizes, hierarchies, and field offsets adjusted for alignment:
Lisp
soot> (type-info '(test-vector))
=> ((name . "test-vector")
(size . 5) ;; 2 bytes for type header + fields
(fields ("y" 3 int8 ...) ("x" 2 int8 ...) ("type" 0 type ...))
(methods ("len" 2 ...) ("set" 1 ...))
...)
The Goal: To enable the assembler to work with structures directly via access macros. Instead of manual offset calculation (magic numbers), the code should look like this:
Lisp
(ld a (ix (-> cursor x))) (ld l (ix (-> cursor y)))
There is still a lot of work to do to bridge the type system and the code generator before I attempt to migrate the actual driver code to this new design and build system. However, early tests show that this approach radically reduces "human factor" errors.
h2w
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.