Close

Moving from C/ASM Drivers to a Custom Lisp-Based Assembler with a Type System

A project log for XiAleste

XiAleste Next is an 8-bit home computer, which is compatible with software for the Amstrad CPC6128 released on 13 June 1985

h2wh2w 01/26/2026 at 21:280 Comments

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.


Discussions