Entry points
Variables defined under when isMainModule will become global variables, and will fill static RAM. Especially bad if you have iterators, as each one will use up static RAM. Putting it in a function even when inlined will make the variables use up registers first when it can.
No
when isMainModule: # game code discard
Yes
proc gameCode(): void {.inline.} = # game code discard when isMainModule: gameCode()
Iteration over dereferenced memory
SDCC isn't THAT smart at recognizing this kind of pattern, so it will be reflected in the ASM. It's one case where idiomatic Nim makes the codegen sadder than it is :(
No
for i in 0xc000'u16..0xd000'u16: cast[ptr byte](j)[] = 0
This one makes code like:
; ... ld bc, #0xc000 00110$: ld e, b ld d, #0xd0 xor a, a cp a, c ld a, #0xd0 sbc a, b bit 7, e jr Z, 00146$ bit 7, d jr NZ, 00147$ cp a, a jr 00147$ 00146$: bit 7, d jr Z, 00147$ scf 00147$: jp C,_nimTestErrorFlag ld l, c ld h, b ld (hl), #0x00 inc bc jr 00110$
Yes
var i = 0xc000'u16 while i < 0xd000'u16: cast[ptr byte](i)[] = 0 i += 1'u16
This one makes code like:
; ... ld bc, #0xc000 00104$: ld a, b sub a, #0xd0 jr NC, 00106$ ld l, c ld h, b ld (hl), #0x00 inc bc jr 00104$ 00106$:
Function arguments
SDCC can only support a maximum of 2 arguments before resorting to the stack, even if they are all 8-bit. The exception is if the first argument is 32-bit, other arguments will immediately be thrown into the stack. See the SDCC manual, pages 75–76.
Nim arguments map neatly to C arguments, so this same limitation applies.
Increment and decrement
Nim natively provides an inc and dec operator, defined for enum types including integers, to be used in place of statements like something += 1. But, by default, they are compiled the same way:
inc something
will be compiled into:
something += (NI) 1;
For value types, SDCC will compile it in the exact same way as something++;. However, for variables where indirect memory access is needed, it turns out SDCC will compile something += 1; and something++; quite differently, where the latter is usually more efficient. (Assuming that --max-allocs-per-node isn't very high, which you should keep that way to keep compile times low).
This module offers the incdec: inc and incdec: dec overrides for this reason. Simply import this module, no code changes are needed.
import jibby/utils/incdec
Printing numbers
Nim's default integer-to-string conversion facilities (itoa(), in C parlance) works by promoting the number into an uint64, and then performing division on that number to determine which digits are to be displayed.
Modern-day processors (even "embedded" ones) are powerful enough to handle this. The Game Boy? Not so much. For one, it is an 8-bit processor—it's already a stretch to handle 32-bit numbers, let alone 64. In fact, SDCC (and GBDK 2020) does not even provide for this architecture a function to perform this division for good reason—it will whine about things like _divulonglong and mulint not being implemented.
Therefore, to do this kind of thing, a specialized implementation is needed in the form of SDCC's itoa() family of functions or another custom implementation, support for both of which are available in the itoa: itoa module:
import jibby/utils/itoa
This module will also automatically override the default itoa: $ operator for integers, so you don't really need to do anything else there.