SpectNet IDE

Visual Studio 2017/2019 integrated ZX Spectrum IDE for the Community

Z80 Assembler » Structures

SpectNetIDE allows you to use structure definitions and placements in your Z80 programs. If you know the struct construct from C, C++, or C#, the concept in Z80 is only partly similar.

Understanding Structures

In SpectNetIDE assembler, a structure definition is a placeholder that defines a byte pattern like this:

Object2D: .struct
    .defw 0
    .defw 0
    .defb 1
    .defb 1
  .ends

This definition says that Object2D is a structure of six bytes (two 16-bit words and two 8-bit bytes) with the following bytes emitted: #00, #00, #00, #00, #01, #01. Whenever you place a structure declaration in a program, just like in this sample, the assembler will emit the bytes you specified in the .struct definition:

  Object2D() ; emits #00, #00, #00, #00, #01, #01

As you can see, a set of six subsequent bytes do not tell enough about the semantics of Object2D. When we created this structure, the original concept was to use two 16-bit numbers to specify the X and Y coordinates of the object, plus two 8-bit numbers to describe its horizontal and vertical velocity. With field definitions (see X, Y, DX, and DY), the meaning of Object2D is more straightforward than it was before:

Object2D: .struct
    X: .defw 0
    Y: .defw 0
    DX: .defb 1
    DY: .defb 1
  .ends

Of course, we would like to initialize objects with different states. With field initializers, we can define structures with initial states that are different from the .struct definition:

Apple: Object2D()
  X -> .defw 100
  Y -> .defw 100

Pear: Object2D()
  DX -> .defb -1
  DY -> .defb -1
  ; Some other code
  ; ...
  ld hl,Apple
  ld de,Pear

In this sample, the Apple label (that the code later loads into HL) points to an Object2D declaration that holds 100 in its X and Y values. Another label, Pear points to a different instance of Object2D (later the code loads that address into DE). Pear has a converse velocity compared to Apple.

The SpecNetIDE assembler allows you initialize structures with any pragma that emit bytes to the assembly output. For example, the following code snippet sets a new Object2D structure in a particular way:

Banana: Object2D()
  -> .defb 10, 1
  -> .defb 12, 2
  DX -> .defb 2, 2

The first .defb pragma (right after the first ->) emits two bytes, 10, and 1, respectively – it sets the X field to 266 (10 + 1256). The second .defb sets Y to 524 (12 * 2256). The initialization of DX (DX -> .defb 2, 2) emits two bytes and sets both DX and DY to 2.

Structure Definition

You can place a structure definition between the .struct and .ends statements. Each structure must have a unique name that you can declare with a label. The compiler accepts all of these definitions:

; Version #1
MyStruct .struct
  ; ...
  .ends

; Version #2
MyStruct: .struct
  ; ...
  .ends


; Version #3
MyStruct
  .struct
  ; ...
  .ends

; Version#4
MyStruct:
  .struct
  ; ...
  .ends

Nonetheless, it raises an error if you do not name the structure:

.struct ; ERROR: .struct must have a name
; ...
.ends

Note: The assembler accepts the following alternative keywords for .struct: .STRUCT, struct, or STRUCT. Similarly, .ends has these aliases, too: .ENDS, ends, ENDS. Though you can define an empty structure, there is no practical reason to do so.

In the body of the structure, you can use only one of these byte-emitter pragmas: .defb, .defw, .defm, .defn, .defc, .defs, .fillb, .fillw, .defg, or .defgx. If you try to use any other construct, the compiler raises an error message.

As you saw earlier, you can specify field labels within the structure body. The assembler is flexible: you can omit field labels, or even use multiple labels for a single field:

Object2D_A: .struct
  Coords:
    X: .defw 0
    Y: .defw 0
  Velocity:
    .defb 1
    .defb 1
  .ends

Here, Object2D_A has two field name for the first .defw field, Coords, and X, respectively. The last .defb does not have its own field name, unlike the one before, Velocity.

Labels and Field Names

The label assigned to the .struct definition has a dual role. When used in a structure initialization (for example, as Object2D is utilized in the Object2D() initializer, it identifies the structure. Nonetheless, you can use the label name in Z80 instructions, too. In this case, the label’s value is the size of the structure. For example, these instructions are equivalent, as the size of the Object2D structure is six bytes:

ld a,Object2D ; Size of Object2D
; ...
ld a,6 ; 

You can allocate a memory block for 100 uninitialized instances of Object2D with this pragma:

My100Objects:
  .defs 100 * Object2D

Note: You cannot assign a label to the .ends statement. If you do, the compiler throws an error.

Labels assigned to the body of the .struct definition are used as field names. You can use them only with the structure name:

Object2D: .struct
    X: .defw 0
    Y: .defw 0
    DX: .defb 1
    DY: .defb 1
  .ends

Apple: Object2D()
  ; ...
  ld hl,Apple + Object2D.DX
  ld a,(hl)

Field label values contain the offset of the particular field from the beginning of the structure. Thus, the last two instructions in the code above load the content of the Apple structure’s DX field into A.

According to this definitions, here are the field label values of Object2D:

Object2D.X: 0
Object2D.Y: 2
Object2D.DX: 4
Object2D.DY: 5

Structure initialization

The .struct definition does not emit any code, it just tells the compiler the layout and initial contents of a structure. To allocate space for a particular structure, you need to initialize it with the name of the structure plus a pair of opening and closing parentheses, just like these samples show:

; Initalize an Object2D
MyObject: Object2D()

; Initialize another one
OtherObject: Object2D()

When you add a label to the structure initialization, that label’s value points to the beginning of the structure in the memory.

start: .org #8000

DistanceEntry:
  .struct
    Address: .defw $
    SeqNo: .defb Index
  .ends


Data: .org #9000
Index = 1;
Entry1:	DistanceEntry()
Index = 2;
Entry2:	DistanceEntry()
Index = 3;
Entry3:	DistanceEntry()

The compiler emits the initialization code for Entry1, Entry2, and Entry3 as if you wrote this:

Data .org #9000
Entry1:
  .defw #9000
  .defb 1
Entry2:
  .defw #9003
  .defb 2
Entry3:
  .defw #9006
  .defb 03

Field Initializers

Field initializers can be used to change the default structure initializer. A field initializer has this syntax:

[identifier] -> byte-emitter-pragma

For a moment let’s forget about the fact that identifier is optional. When you use it, it must be one of the structure’s fields names. The byte-emitter-pragma is one of the SpectNetIDE pragmas you can use to define a structure body; namely these: .defb, .defw, .defm, .defn, .defc, .defs, .fillb, .fillw, .defg, or .defgx.

Note: You can use the label syntax for field names, so you can add an optional colon after the identifier.

You can apply field initializer statements right after the structure initialization. Any other Z80 instruction, pragma, statement, directive signs the end of the structure initialization. This code snippet shows two examples of setting up Object2D structures. The first is correct, however, the second one raises an error:

Object2D: .struct
    X: .defw 0
    Y: .defw 0
    DX: .defb 1
    DY: .defb 1
  .ends

Obj1: Object2D()
  DX -> .defb 2
  DY -> .defb 2
  ld a,b
  ; ... Some other code
Obj2: Object2D()
  X -> .defw 100
  Y -> .defw 100
  ld hl,#4000    ; Field initialization stops here
  DX -> .defb 4  ; ERROR

The compiler does not care how you initialize fields. For example, even if you have created the X field of Object2D with a .defw pragma, you can set its value with .defb, like in this example:

Obj3: Object2D()
  X -> .defb 100, 0

You can even initialize two fields with a simple initializer statement. For example, DX and DY are one byte each. You can initialize both of these fields in a single step, as these code snippet shows:

Obj4: Object2D()
  DX -> .defb 2,2

Obj5: Object2D()
  DX -> .defw #0202

The order of fields is unimportant, you can initialize them in any order:

Obj6: Object2D()
  DX -> .defb 1
  X -> .defw 100
  DY -> .defb 1
  Y -> .defw 200

Fluent Structure Initialization

The assembler allows using flexible initialization, where you do not use field names. The compiler amits bytes as the byte emitter pragmas would do if you were not within a structure initialization. Let’s assume, you initialize an Object2D this way:

Obj7: Object2D()
  -> .defb 1, 0
  DX -> defw #0303

Without the field initializers the structure would contain these six bytes:

#00, #00, #00, #00, #01, #01

However, the field initializers overwrote the default bytes with the ones displayed in boldface:

#01, #00, #00, #00, #03, #03

You can choose your preferred way to initialize a structure using field or unnamed initializers. The compiler does not care how you assemble the set of bytes within the structure. However, it does not allow you to overflow the structure boundaries. This sample shows you two initialization of Object2D. The first is correct, as it emits exactly six bytes. However, the second raises an error, since the initialization tries to put eight bytes into the structure:

Obj8: Object2D()
  -> .defm "012345"

Obj9: Object2D()    ; ERROR: The code tries to intialize the structure with 8 bytes
  X -> .defw 100
  -> .defm "012345"