The Klive Z80 Assembler
The original goal of the Klive Assembler was to have a simple tool that allows you to compile Z80 assembly code and inject it into the ZX Spectrum virtual machine. As the community has started using it, I've been receiving feature requests to add some helpful capabilities to the Assembler.
Main Features
Here is a list of essential features the Klive Assembler supports:
- Full Z80 instruction set, including the initially undocumented Z80 registers and instructions
(such as the 8-bit halves of
ix
andiy
, namelyixl
,ixh
,iyl
,iyh
). - ZX Spectrum Next extended Z80 instruction set
- Alternate syntax versions. All directives, pragmas, and statements have multiple versions; you can use your preferred notation. For example, you can use
.loop
,loop
,.LOOP
orLOOP
to declare a loop. All of the.defb
,DEFB
,.db
,DB
(and a few other) tokens can be used for defining byte data. - Z80 Preprocessor. With preprocessor directives, you can execute conditional compilation and include other source files, inject symbols for debug time, and run time compilations separately. In Klive you can use powerful macros, too. Nonetheless, they are not preprocessor constructs (see below).
- Fast compilation. Of course, it depends on the code, but the compiler can emit code for about ten thousand source code lines per second (MacBook Pro).
- Rich expressions. The compiler can handle most arithmetic and logic operators in C, C++, C#, Java, and JavaScript. You can use integer, float, and string expressions. The language supports more than 40 functions that you can use in the expressions (e.g.,
Amp * sin($cnt * Pi() / 16)
) - Rich literal formats. Decimal, float, hexadecimal, binary, and string literals are available.
You can use multiple variants for hexadecimal numbers (
$12ae
, #12AE, 0x12AE, 12AEh), and binary numbers (0b00111100, %00111100, %0011_1100). In strings, you can use ZX Spectrum-specific escape codes, for example,\i
for INK,\P
for the pound sign, and many others. - Assembler control flow statements. You can use loops (
loop
,repeat
..until
,while
..wend
,for
..next
) and conditional statements (if
) to create an assembler control flow. These constructs can be nested and provide local scope for labels, symbols, and variables. - Powerful dynamic macros. You can create macros with arguments. In the macro bodies, the current values of arguments can replace entire instructions, operands, or parts of expressions. Moreover, through arguments, you can inject multiline instructions and statements into macro declarations.
- Modules. You can use modules to serve both as logical containers to separate partitions of the code and namespaces to create scopes for labels and symbols.
How The Assembler Works
The assembler compiles the code in these phases:
-
It takes the source code and runs a preprocessor that parses the entire code and applies the directives in the code. You can easily recognize directives starting with
#
, such as#ifdef
,#endif
,#define
,#include
, and others. During the preprocessing phase, the assembler detects the syntax errors and loads and processes the included files. The result is a digested syntax tree that does not contain directives anymore, only instructions, pragmas, and statements. -
The assembler collects macro definitions and stores their syntax tree to later use them when macros are invoked with their actual parameters.
-
The assembler goes through the digested syntax tree and emits code. During this operation, it must evaluate expressions to resolve symbols and identifiers to their actual values. Because the assembler progresses from the first line to the last, it may happen that it cannot get the value of an identifier, which is defined somewhere later in the code. When the assembler detects such a situation, it notes it and creates a fixup entry.
-
The assembler goes through all fixup entries and resolves symbols not defined in the previous phase. Of course, it might find unknown symbols. If this happens, the assembler reports an error.
Note: Several pragmas and statements intend to evaluate an expression in phase 3. If they find an unresolved symbol during that phase, they do not create a fixup entry but immediately report an error.