Topics

UTTORI ASM ยท IN DEVELOPMENT

SNES labels and structs

Use snes labels and structs in SNES projects, with syntax and behavior notes.

On this page

SNES behavior

Main, Local, and Static Labels

A colon defines a normal label at the current logical PC:

PlayerUpdate:
  lda PlayerState
  rtl

Dot labels are scoped below the current parent:

PlayerUpdate:
.loop:
  dex
  bne .loop

Multiple dots walk deeper hierarchy levels in supported Asar-style source. A plain assignment creates a static numeric label rather than a PC label:

ScreenWidth = 256

Static labels can be used where a resolved numeric symbol is required. global Label: marks a label as global in namespace-sensitive source.

Relative Labels

Runs of + and - create anonymous forward and backward targets. The number of signs is the depth:

  beq +
  lda #$01
+:

-:
  dex
  bne -

Macro-local labels use ? or # forms and receive unique expansion identities, preventing two invocations from stapling their branches together. See macrolabels.asm.

Namespaces

namespace Audio
Upload:
  rtl
namespace off

jsl Audio_Upload

namespace name prefixes labels with name_. namespace off or bare namespace returns to the empty namespace. pushns and pullns preserve and restore the namespace. pullns without a matching push is an error.

Nested mode builds a path:

namespace nested on
namespace UI
namespace Inventory
Draw:
  rtl
namespace off
namespace off
namespace nested off

jsl UI_Inventory_Draw

namespace nested off also clears the current nesting path. The braces used in namespaces.asm are visual compatibility no-ops. The namespace directives do the real work.

Structs

Structs define addressable layouts without emitting bytes:

struct DMA $4300
  .control:     skip 1
  .destination: skip 1
  .source_low:  skip 1
  .source_high: skip 1
  .source_bank: skip 1
  .size_low:    skip 1
  .size_high:   skip 1
endstruct align $10

stz DMA[2].control
lda #$18
sta DMA[2].destination

Syntax:

struct name [base]
  .member: skip size
endstruct [align alignment]

The base defaults to zero. align rounds the final size up, which also controls array stride. Members can be nested through dot labels. Structs can extend another struct:

struct Actor $7E2000
  .x: skip 2
  .y: skip 2
endstruct

struct Enemy extends Actor
  .health: skip 2
endstruct

The parent reserves enough stride for its largest extension, so Actor[3].Enemy.health lands in the correct record. countof(name) returns the declared record count (one when unbounded), bytesof(name) its total byte extent, and startof(name) / endof(name) its physical first/exclusive-end addresses, accounting for a nonzero first index. sizeof(name) returns the defined struct size. objectsize(name) follows the expression host's object-size rules. Examples are in structs.asm and the implementation is struct-engine.ts.

Typed Fields & Named Initializers

Uttori also accepts typed members and complete named values. Typed fields let one struct definition describe both member addresses and how to encode record values. dataStruct emits one complete named record. dataStructs emits a sequence of explicitly indexed records. These are Uttori extensions. Existing Asar struct, skip, extends, endstruct align, member expressions, sizeof() and objectsize() remain available and keep their existing address/stride behavior:

struct MapOffsets $CD0004
  .offset: dw
endstruct count 1023 firstindex 1

org $CD0006
dataStructs MapOffsets
  [1] = { offset = $0804 }
  [2] = { offset = $080B }
endDataStructs

dataStruct MapOffsets[1], { offset = $0804 } emits a single record. Types declare widths without emitting bytes. Initializers write unsigned little-endian values in layout order. All storage fields are required. Arrays (db[4], dw[2]) require explicit value lists. Legacy skip fields require raw byte lists. The emitter checks field names, widths, array lengths, consecutive table indexes and pc(). Optional bounds affect initializers without changing existing Asar address expressions.

Declare Fields

Declaration Inside a Struct Meaning Initializer Value
.flags: db One unsigned 8-bit integer $80
.hp: dw One unsigned 16-bit integer $1234
.address: dl One unsigned 24-bit integer $C08000
.mask: dd One unsigned 32-bit integer $FFFFFFFF
.palette: dw[3] Three 16-bit integers [$001F, $03E0, $7C00]
.unknown: db[2] Two explicit bytes [$AB, $CD]
.legacy: skip 2 Existing opaque two-byte member [$AB, $CD]

All scalar encodings are little-endian. Arrays are contiguous and use their element width. db[1] is still an array and requires [value], not a scalar. Field names retain their case-sensitive struct spelling. Directive and type keywords are case-insensitive.

Array lengths must be positive integers known when the definition is processed. A typed field and an initialized record are limited to 65536 bytes. Initializers accept unsigned integers only: for all-one bits, use $FF, $FFFF, etc., rather than -1. Values are not silently truncated. Ordinary Asar db/dw directives retain their existing behavior.

Do not put default values after field types: .hp: dw $1234 is invalid. Declare the width in the struct and supply the value at the emission site. skip 2 does not imply dw: it remains an opaque byte array for initialization. Plain labels may alias field addresses, but aliases are not additional storage and must not appear as initializer keys.

Tables, Bounds & Alignment

endstruct [align N] [count N] [firstindex N]

align retains Asar's size/stride rounding.

count adds optional bounds for named initializers.

firstindex defaults to zero and requires count. It does not adjust the struct base. Bounds do not restrict existing address expressions, so existing assembler operands can still use biased bases or compute an address beyond the initialized range.

dataStructs Name / endDataStructs is a non-empty data-only block. Each row is [index] = { field = value, ... }. Duplicate, decreasing or skipped indexes fail. Labels, org, includes, conditionals, macros and nested tables cannot appear inside the block. Put a complete block inside a macro or conditional instead. To label a record boundary, close the block, put the label at the current position, then start the next block.

Alignment bytes must also be explicit:

struct Aligned $808000
  .value: db
endstruct align 4

org $808000
dataStruct Aligned, { value = 1, __padding = [$AB, $CD, $EF] }

__padding is the reserved initializer name for trailing alignment bytes, only when alignment actually adds bytes. It is not an additional address symbol. For ordinary unknown regions, use named db[N] fields. The emitter rejects overlapping or incomplete storage layouts rather than inventing bytes.

Named initialization supports flat records and scalar arrays. It does not initialize Asar extension/overlay layouts, nested record values, bitfields, strings, or signed types. Asar extensions remain usable for address calculations and ordinary raw data emission. Named initialization reports an explicit error for a layout with a parent or extensions.