La Lypémanie Vaut Mieux

By Madram / Overlanders.

After having tried psilocybin, I developed two peculiar hobbies. Cleaning my room and porting languages on the Zilog Z80. One way to do the latter is to compile the language’s interpreter/compiler/virtual machine/whatever (often in C) to Z80. You read it right: cross-compilation.

Nietzsche écrivait dans une correspondance privée à Freiherr Karl Von Gersdorff : le cross-development (Croß-Entwicklung) est l’apanage des faibles, et le plus sûr marqueur du déclin d’une civilisation. D’un autre côté, Nicolas Flamel aimait à répéter : "Transformer la rouille en octobit, quelle poilade !".

De toute façon, je tiens à rassurer mon thérapeute : je cross-compile, mais ne suis pas tombé dans l’indigence et l’auto-complaisance du cross-dev. Ici, je dev’ que dalle.

Je le redis, le but poursuivi est de compiler un compilateur existant (ou une VM — Lua, Smalltalk, etc.) à destination du Z80, de sorte à utiliser le langage directement sur un CPC 6128. Cela étant dit, les étapes pour y parvenir intéresseront certainement les gross-developers et la communauté Z80 en général. Avantage collatéral.

As a prerequesite for my quest (a prerequeste), I wanted to establish the state of the art concerning compilers targeting Z80, in particular comparing SDCC and LLVM-Z80.

Puis je me suis rappelé de la 43ème règle de Jordan Peterson : "life is too short to waste it on PC". Il parlait peut-être d’autre chose.

LLVM has many advantages:

  • modern and neat architecture
  • great optimizations (before code generation)
  • more importantly, frontend (language D, rust, C, … -> IR) and backend (IR -> assembler) are independent, meaning that any language producing LLVM IR can be used to generate Z80 (ActionScript, Ada, C#, Common Lisp, Crystal, CUDA, D, Delphi, Dylan, Fortran, Graphical G, Halide, Haskell, Java bytecode, Julia, Kotlin, Lua, Objective-C, OpenCL, PostgreSQL’s SQL and PLpgSQL, Pure, Ruby, Rust, Scala, Swift, Vuo, Xojo and other).

Step 1: Getting compiling this LLVM-Z80 fork.

~ $ git clone https://github.com/jacobly0/llvm-project.git z80
~ $ mkdir z80/build
~/z/build (z80|✔) $ cd z80/build
~/z/build (z80|✔) $ cmake -DCLANG_ENABLE_STATIC_ANALYZER=1 
                          -DLLVM_ENABLE_ASSERTIONS=On
                          -DCMAKE_BUILD_TYPE=RelWithDebInfo
                          -DCMAKE_INSTALL_PREFIX=$HOME/local
                          -DLLVM_ENABLE_PROJECTS="all"
                          -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=Z80
                          -DLLVM_TARGETS_TO_BUILD= 
                          -DBUILD_SHARED_LIBS=ON
                          -G "Ninja" ../llvm

Step 2: Trying it on a simple example.

Je me suis inspiré de l’article suivant, sans autorisation de l’auteur, que je n’ai pas réussi à contacter, faute de motivation.

////////////////////////////////////////////////////////////////////////
// Mochilote - www.cpcmania.com
// Test02sdcc.c.  (Simplified by mdr) 
////////////////////////////////////////////////////////////////////////

unsigned int TotO()
{
  unsigned int nCounter = 0;
  unsigned int nLoops = 0;

  for(nCounter = 0; nCounter < 65535; nCounter++)
    nLoops++;

  return nLoops;
}
////////////////////////////////////////////////////////////////////////

Pour référence, SDCC donnait alors un truc du genre:

  ld  de,#0x0000
  ld  bc,#0xFFFF
00106$:
  inc de
  dec bc
  ld  a,b
  or  a,c
  jr  NZ,00106$

C’était en 2003, ça a pu s’améliorer.

Entering:

~/z/build (z80|✔) $ ./bin/clang -target z80 -xc -O2 -S  Test02sdcc.c -o test.asm

LLVM gives (don’t mind the boilerplate):

  section .text,"ax",@progbits
  section .text,"ax",@progbits
  public  _toto
_toto:
  ld  hl, -1
  ret
  section .text,"ax",@progbits

  ident "clang version 12.0.0 (ovl rules)"
  extern  __Unwind_SjLj_Register
  extern  __Unwind_SjLj_Unregister

Le compilateur a optimisé toute la boucle. Exactement ce qu’on attend de lui. Du coup, des tests plus poussés deviennent nécessaires.

Pour la petite histoire, c’est déjà le genre de choses que faisait le compilateur de Digital Mars (par Walter Bright, créateur du beau langage D) il y a plus de 30 ans. Un journaliste de l’époque avait voulu tester la rapidité du code généré, en chronométrant une boucle dans le même genre comprenant des millions d’itérations. Le compilateur ayant détecté que la boucle ne faisait rien, il l’avait supprimée et l’exécution était immediate. Le journaliste en avait déduit que le compilateur était buggé ! Comme le disait Carlos, les médiocres ont du mal a reconnaître l’excellence.

Next step: make sure neat call-convention is used, avoiding the use of the stack and index registers to pass parameters.