The Layout System
In the following example, a raw sprite table stores four bytes per sprite: an x position, a y position, a tile index and a flags byte. When .equ constants define the field offsets, inserting a field makes every following constant and its access expressions stale.
AZM's layout system replaces those manual constants with one record declaration. sizeof and offset then derive byte counts and field positions from its field list.
Record offsets
Written by hand, a sprite record needs one .equ per field offset:
SPRITE_X .equ 0
SPRITE_Y .equ 1
SPRITE_TILE .equ 2
SPRITE_FLAGS .equ 3
SPRITE_SIZE .equ 4
Sprites:
.ds 16 * SPRITE_SIZEAdding a field between SPRITE_TILE and SPRITE_FLAGS changes the required values of both SPRITE_FLAGS and SPRITE_SIZE.
A type declaration replaces the manual constants:
Sprite .type
x .field byte
y .field byte
tile .field byte
flags .field byte
.endtype
Sprites:
.ds Sprite[16]sizeof(Sprite) evaluates to 4. offset(Sprite, flags) evaluates to 3. A new field between tile and flags updates both values automatically. A mistyped field name such as offset(Sprite, flagz) is rejected at assembly time.
Scalar types, sizeof and arrays
Three scalar names are the building blocks for field sizes:
| Name | Byte count |
|---|---|
byte | 1 |
word | 2 |
addr | 2 |
These names are valid in size positions: inside .type / .union declarations and as .ds operands. word and addr have the same size. addr documents that a field is intended to contain an address; AZM currently applies no separate address type checking.
sizeof(Type) returns the exact packed byte count for a type. The result is an ordinary integer constant, valid anywhere an expression is valid:
sizeof(byte) ; 1
sizeof(word) ; 2
sizeof(Sprite) ; sum of Sprite's field sizes
SPRITE_SIZE .equ sizeof(Sprite)
TOTAL_RAM .equ MAX_SPRITES * sizeof(Sprite)A type followed by a bracket count forms an array type expression:
byte[32] ; 32 bytes
word[8] ; 16 bytes
Sprite[16] ; sizeof(Sprite) * 16 bytesArray type expressions appear in .ds operands, .field declarations and sizeof / offset arguments:
.ds byte[32] ; same as .ds 32
.ds Sprite[16] ; same as .ds sizeof(Sprite) * 16The count inside the brackets of a .ds operand must be a numeric literal. For a named count, multiply explicitly:
MAX_SPRITES .equ 16
.ds MAX_SPRITES * sizeof(Sprite)When the count is needed as a numeric constant (for a .equ, for example), sizeof provides it: SIZE .equ sizeof(byte[32]). .equ needs a numeric value, not a type expression.
Records with .type
A record type is a .type layout with named fields.
Field declarations
A .type declaration uses the name-left form, with the record name first, then .type. Inside the block, .field declares one named field. The token after .field is the field's layout type expression:
Sprite .type
x .field byte
y .field byte
tile .field byte
flags .field byte
.endtypeEach field has a name, a size and an offset the assembler computes by summing the preceding fields:
| Declaration | Meaning |
|---|---|
name .field byte | 1-byte field |
name .field word | 2-byte field |
name .field TypeExpr | field of any layout size |
AZM also provides concise forms for the three scalar field sizes:
| Declaration | Equivalent form |
|---|---|
name .byte | name .field byte |
name .word | name .field word |
name .addr | name .field addr |
The explicit .field form is required when the size is a type expression, such as an array or a nested record type:
Buffer .type
data .field byte[256] ; 256 bytes
cursor .field word ; 2 bytes
.endtype
Actor .type
pos .field Sprite ; nested record
state .field byte
timer .field word
.endtypeAfter the declaration, sizeof and offset provide the assembler-time constants:
SPRITE_SIZE .equ sizeof(Sprite) ; 4
SPRITE_X .equ offset(Sprite, x) ; 0
SPRITE_Y .equ offset(Sprite, y) ; 1
SPRITE_TILE .equ offset(Sprite, tile) ; 2
SPRITE_FLAGS .equ offset(Sprite, flags) ; 3An .equ line is useful when a name appears in multiple places. A one-off constant can remain a direct sizeof or offset expression in its operand.
Allocating and accessing records
A .ds declaration allocates a single record, whose fields are accessed through offset constants:
Player:
.ds Sprite ; sizeof(Sprite) bytes, uninitialized
ld ix,Player
ld a,(ix + SPRITE_X)
inc a
ld (ix + SPRITE_X),aMultiplying the record size allocates an array of records:
SpriteTable:
.ds Sprite[16]Accessing element N at assemble time, when N is a constant:
N .equ 3
ld hl,SpriteTable + N * sizeof(Sprite) + SPRITE_FLAGS
ld a,(hl)Runtime indexing requires explicit address arithmetic when the index is in a register:
; A = sprite index (0..15)
ld hl,SpriteTable
ld b,0
ld c,a
add hl,bc
add hl,bc
add hl,bc
add hl,bc ; HL = SpriteTable + A * 4Nested fields and array paths
When a record embeds another record, offset reaches through both layers with a dotted path:
Actor .type
pos .field Sprite
state .field byte
.endtype
ACTOR_POS_X .equ offset(Actor, pos.x) ; 0
ACTOR_POS_Y .equ offset(Actor, pos.y) ; 1
ACTOR_STATE .equ offset(Actor, state) ; sizeof(Sprite)offset also accepts an array index step inside the path:
offset(Sprite[16], [2].flags)This returns the byte offset of the flags field of element 2: 2 * sizeof(Sprite) + offset(Sprite, flags). The index must be a numeric literal.
ELEM2_FLAGS .equ offset(Sprite[16], [2].flags)
ld hl,Sprites + ELEM2_FLAGS
ld a,(hl)Named aliases with .typealias
A .typealias declaration gives a name to any layout type expression. The declared name is a transparent assembler-time alias: the assembler substitutes the full type expression at every use.
SpriteArray .typealias Sprite[16]SpriteArray is valid anywhere a type expression is valid:
Sprites:
.ds SpriteArray
SIZE .equ sizeof(SpriteArray)
FLAGS .equ offset(SpriteArray, [3].flags)
ld hl,<SpriteArray>Sprites[3].flagssizeof(SpriteArray) returns the same value as sizeof(Sprite[16]), and the cast path <SpriteArray>Sprites[3].flags expands to Sprites + offset(Sprite[16], [3].flags).
A .typealias names the array type directly, so with SpriteArray .typealias Sprite[16] the cast path to element 3's flags field is [3].flags. A wrapper record with a .field declaration adds an extra path level:
SpriteArray .type
sprites .field Sprite[16]
.endtypeWith that declaration, the same field requires .sprites[3].flags; the .sprites step is part of the type structure.
Cast syntax
A layout cast applies a particular layout to an address while AZM calculates field offsets. The cast is evaluated entirely at assembly time and does not change the emitted bytes:
ld hl,<Sprite>Player.flags
ld hl,<Sprite[16]>Sprites[3].flagsThe structure is <TypeExpr>base[index].field. <TypeExpr> is the layout, base is a label or address expression, each [index] is an array step and each .field is a field-name step. These two lines produce the same assembled bytes:
ld hl,Sprites + (3 * sizeof(Sprite)) + offset(Sprite, flags)
ld hl,<Sprite[16]>Sprites[3].flagsParentheses perform memory access; the cast path itself resolves to an address:
ld a,(<Sprite[16]>Sprites[3].flags) ; load byte at that address
ld hl,<Sprite[16]>Sprites[3].flags ; load the address itself into HLIndices inside a cast path must be assembler-time constant expressions:
IDX .equ 3
ld hl,<Sprite[16]>Sprites[IDX].flags ; valid: IDX is a constant
ld hl,<Sprite[16]>Sprites[HL].flags ; error: HL is not a constantDot notation reaches nested record fields by the same rules:
ld hl,<Actor>Player.pos.x
; Equivalent to:
ld hl,Player + offset(Actor, pos.x)Unions and alternate views
A union describes multiple overlapping views of the same bytes. All union members start at offset zero; the union's size is the size of its largest member. The following packed value provides both byte and 16-bit views:
PortValue .union
status .field byte ; byte-wide access
full .field word ; word-wide access
.endunion
IoPort .type
ptr .field word
value .field PortValue
.endtype
Port: .ds IoPortCast syntax reaches union members by the same rules as record fields:
ld a,(<IoPort>Port.value.status) ; read the status byte
ld hl,(<IoPort>Port.value.full) ; read the full word