gbdev / rgbds

Rednex Game Boy Development System - An assembly toolchain for the Nintendo Game Boy and Game Boy Color
https://rgbds.gbdev.io
MIT License
1.33k stars 175 forks source link

Allow declaration of data structures #98

Open PikalaxALT opened 7 years ago

PikalaxALT commented 7 years ago

Definition: foo_struct: STRUCT foo, bar

Declaration: foo_struct Name, $01, $2345

Use: dbw Name.foo, Name.bar

AntonioND commented 7 years ago

I think that you can pretty much get the same effect with macros.

ELEMENT : MACRO
\1::
    DB \2
    DW \3
ENDM

    ELEMENT element1, 1, 1234
    ELEMENT element2, 1, 1234

    ld a,[element1+0] ; etc

Yes, sure, it is not very convenient to get the value of a specific field, but that's how I usually do this.

pinobatch commented 5 years ago

Does ISSOtm/rgbds-structs do what you want?

Rangi42 commented 3 years ago

Sjasm has structure support similar to how rgbds-structs works.

Rangi42 commented 3 years ago

Here's a rough first draft for how structs might be implemented in rgbasm. I'm trying to reuse existing keywords and keep the syntax similar to what's already there. Suggestions welcome.

struct Foo begins defining a structure's members; endstruct stops that. Inside, you declare labels and allocate space with db/dw/dl/ds as if you're in a RAM section; union/nextu/endu would also be supported. This would define offset constants, rsset-style (although without affecting the value of _RS). No labels are defined and no space is allocated yet though.

Inside a RAM section, you can use dstruct Foo like the db/dw/dl/ds directives. It will then define the struct's member labels and allocate space, prefixing each label with the closest defined label above (which could be a .local one). If that above label is :: exported, so will be all the members.

struct can take a second argument like struct Foo, _ or struct Bar, _m for the separator between its prefix and each member suffix. This would make it compatible with different projects' styles of labeling.

I'm not sure about how sizeof should work yet. TBH I'd be fine without it; you can define a member at the very end of your struct for that, and then do either e.g. ld bc, NPC_Sizeof or ld bc, Player_Sizeof - Player.

Another problem: it would be nice to allow . in member names for creating local member labels (e.g. person.x, person.y, person.z), but a . isn't allowed in constant names for the offset constants.

Example 1:

struct Color
R: db
G: db
B: db ; should not get confused with register B
End:
endstruct

wColorData:
    dstruct Color
.two:
    ds 1 ; padding
    dstruct Color

This would act like:

ColorR EQU 0
ColorG EQU 1
ColorB EQU 2
; TODO: SIZEOF_Color ? #Color ? function-style SIZEOF(Color) ?

wColorData:
wColorDataR: db
wColorDataG: db
wColorDataB: db
wColorDataEnd:
.two:
    ds 1 ; padding
.twoR: db
.twoG: db
.twoB: db
.twoEnd:

Example 2:

struct NPC, _
    YPos:  dw
    XPos:  dw
    YBox:  db
    XBox:  db
    GfxID: db 2
    union
        MovementData: dl 2
    nextu
        AnimData: dl 1
        PalData:  dstruct Color
    endu
endstruct

wMapID::   ds 2
wTileset:: db
wPlayer:: dstruct NPC
wRival:   dstruct NPC

This would act like:

wMapID::   ds 2
wTileset:: db
wPlayer::
wPlayer_YPos::  dw
wPlayer_XPos::  dw
wPlayer_YBox::  db
wPlayer_XBox::  db
wPlayer_GfxID:: db 2
UNION
wPlayer_MovementData:: dl 2
NEXTU
wPlayer_AnimData:: dl 1
wPlayer_PalData::
wPlayer_PalDataR:: db
wPlayer_PalDataG:: db
wPlayer_PalDataB:: db
wPlayer_PalDataEnd::
ENDU
wRival:
wRival_YPos:  dw
wRival_XPos:  dw
wRival_YBox:  db
wRival_XBox:  db
wRival_GfxID: db 2
UNION
wRival_MovementData: dl 2
NEXTU
wRival_AnimData: dl 1
wRival_PalData:
wRival_PalDataR: db
wRival_PalDataG: db
wRival_PalDataB: db
wRival_PalDataEnd:
ENDU
ISSOtm commented 3 years ago

Honestly, end<thing> to end blocks is getting kinda ridiculous. Given that this isn't "flow" syntax, I think using something closer to real blocks would be better—if only due to less global state?

Braces are obviously ineligible, but (single) brackets are fair game. I know it would feel very different from the rest of the syntax, but it's also, in concept, nothing like it anyway.

Rangi42 commented 3 years ago

There's if/endc, rept/endr, macro/endm, union/endu, and load/endl. Other pairs are pushc/popc, pushs/pops, and pusho/popo. I would suggest ends instead of endstruct, but the word "ends" seems likely to be used as a name by someone already.

[[ Double brackets ]] are tentatively going to be for inline fragments, which on the one hand is unusual for multi-line constructs, but on the other hand it goes with inline [ dereferencing ] or ( grouping ).

I don't think a new block construct like this should use brace-style delimiters unless the others are being changed to do the same, like rept 42 [[ ... ]] or macro mac { ... }. Which would be a major version update.

"nothing like it anyway": eh, it reminds me of load/endl blocks, where the labels are declared in one spot but end up going in another.

evie-calico commented 3 years ago

I prefer RGBDS Structs' use of bytes, words, and longs over db. A C-like syntax would be ideal either way

struct Structure [
    byte FieldByte
    ; or
    byte[2], FieldWord
]

Then you could do something like this:

u8 EQU byte

struct Enemy [
    u8 health
]
ISSOtm commented 3 years ago

Or just byte[2] FieldWord.

Rangi42 commented 3 years ago

Something like struct Enemy [ byte[2] FieldWord ] looks way too different from typical rgbasm syntax to me.

I'd appreciate if someone could refactor existing WRAM labels into a struct with minimal editing. Like turning this from pokecrystal:

wPlayerSubStatus1:: db
wPlayerSubStatus2:: db
wPlayerSubStatus3:: db
wPlayerSubStatus4:: db
wPlayerSubStatus5:: db

to this:

struct Substatus
SubStatus1:: db
SubStatus2:: db
SubStatus3:: db
SubStatus4:: db
SubStatus5:: db
endstruct

wPlayer:: dstruct Substatus

(That does raise the issue of, "wPlayer is a pretty generic label to define there, people might want it as the prefix to more than one struct." For that the rgbds-structs syntax of dstruct Substatus, wPlayer is better, and there's a parallel between "struct Name, Infix" and "dstruct Name, Prefix".)

evie-calico commented 3 years ago

I'm worried about the readability of that. There isn't much point in implementing structs if they're going to be that similar, since you might as well just use macros.

Our goal should be to break the mold a bit, since RGBDS Structs already does most of this.

ISSOtm commented 3 years ago

If #635 is merged, this would look fine:

struct Substatus [
    SubStatus1:: db
    SubStatus2:: db
    SubStatus3:: db
    SubStatus4:: db
    SubStatus5:: db
]

wPlayer:: dstruct Substatus
Rangi42 commented 3 years ago

That looks alright to me. Maybe use double brackets so they stand out more? Presumably it would support union inside. I'd also go with rgbds-structs' dstruct Substatus, wPlayer as said above.

ISSOtm commented 3 years ago

I don't like that syntax because it fails to convey which is the type and which is the name.

Besides, we'd probably also want initializer syntax for ROM data.

PlayerData: struct Substatus [
    0, 1, 2, 3, 4, ; Allow trailing commas
]

EnemyData: struct Substatus [
    Substatus1 = 8, Substatus2 = 7, Substatus3 = 6, Substatus4 = 5, Substatus5 = 4
]

PeonData: struct Substatus [
    Substatus4 = NPC_SUBSTAT4 ; Commas or newlines, or both
    Substatus2 = HIGH(PEON_CONST), Substatus3 = LOW(PEON_CONST),

    ... = 0 ; Syntax for default init...

    Substatus1 = 69 ; ...not necessarily last
]

(RGBDS-structs has a more clunky syntax for initializers, since it's constrained by macro syntax.)

Rangi42 commented 3 years ago

(I edited those wLabels to LabelData, it was confusing at first because they're not WRAM.)

Agreed that a ROM initializer syntax would be useful too. Although I think dstruct should be used for that, so struct defines a new kind of structure, and dstruct uses an existing structure whether in ROM or in RAM.

evie-calico commented 3 years ago

This was being discussed again in the discord so I figured I'd contribute my own idea for syntax:

STRUCT NPC
  Name:  ds 16 ; Allocate 16 bytes for a name string
  Health: db 10 ; Default of 10 health, allocate one byte
  Logic: far_pointer $0000 ; Supports macros too!
ENDS

SECTION "ROM definition", ROM0
NewNPC: object NPC [
  Name: "<=16 Letter Name" ; Even if this string is <16 characters, the ds will pad it.
  ; Health is left out, so it just defaults to 10
]

BrokenNPC: object NPC [
  Name: "This name is way too long!!!" ; Error/Warning because the string is too large
]

SECTION "RAM definition", WRAM0
RamNPC: object NPC
  ; Nothing further is needed, RAM will just go off of the default values.

This will allow the use of macros and doesn't require the user to initialize every value if they don't need to.

Rangi42 commented 3 years ago

I like that syntax; struct ... ends for definition, struct [[ ... ]] (or object or dstruct) for declaration.

Rangi42 commented 9 months ago

Syntax from WLA-DX for comparison:

.STRUCT water
    name   ds 8
    age    db
    weight dw
.ENDST

.DSTRUCT waterdrop INSTANCEOF water VALUES
    name:   .db "tingle"
    age:    .db 40
    weight: .dw 120
.ENDST

.STRUCT drop_pair
    waterdrops: instanceof water 2
.ENDST

.DSTRUCT drops INSTANCEOF drop_pair VALUES
    waterdrops.1:        .db "qwertyui" 40
                         .dw 120
    waterdrops.2.name:   .db "tingle"
    waterdrops.2.age:    .db 40
    waterdrops.2.weight: .dw 12
.ENDST
Rangi42 commented 9 months ago

And syntax from NASM:

struc   mytype 
  mt_long:      resd    1 
  mt_word:      resw    1 
  mt_byte:      resb    1 
  mt_str:       resb    32
endstruc

mystruc: 
    istruc mytype 
        at mt_long, dd      123456 
        at mt_word, dw      1024 
        at mt_byte, db      'x' 
        at mt_str,  db      'hello, world', 13, 10, 0 
    iend
Rangi42 commented 9 months ago

And from ca65:

.struct Point
      xcoord  .word
      ycoord  .word
.endstruct

P1:      .tag    Point
P2:      .tag    Point
aaaaaa123456789 commented 9 months ago

I should probably comment at this point that struc, at, istruc and the like are all macros in NASM :P

("What do they do", I hear? struc is the easiest to explain: it creates a discarded non-output section with an offset of zero.)

Rangi42 commented 9 months ago

If we come across a third use case, maybe I'll push to actually implement DISCARD.

Edit: Hmm. I think even with DISCARDable sections, we couldn't write a macro to work that way: it can't iterate over all the labels in a section and use them as locals inside another label (e.g. you type field: ds 3 after a struct NPC, then when you do dstruct Jane, NPC it would define Jane.field). You would have to run macros for each label at the point of definition, and then the invisible section isn't actually gaining you anything since you can't use label syntax (have to do bytes 3, field as rgbds-structs already does).