Friday, September 28, 2012

Tracking Local Variable Usage

I'm trying to learn to code for the NES, and one thing that is obvious very quickly is that oine has to take advantage of the zero page memory. It's faster for one, but if you have multiple subroutines, it's better to reuse that memory. The idea is that if a subroutine needs memory, give it some zeropage ram and reuse it with another sub later. If a subroutine needs something to be static, assign it memory explicitly in the zeropage or bss segment. So I was reusing zeropage for subroutine locals and I got paranoid that I was going to overwrite something or run into some problem or another, so I invented a pretty easy to use method of tracking local variable ram usage and paramater ram usage (since I also use zeropage to pass paramaters). It does NOT use a stack, it just gives you warnings via NintendulatorDX's debugOut macro when you might be doing something dangerous. Use in code is as follows:
; In a main module, define:

FUNCTION_RANGE_CHECKING_ON = 1 ;Turn off if functions are okay
FUNCTION_LOCAL_SIZE = $0A ;# bytes for local zero page shared
FUNCTION_PARAMS_SIZE = $06 ;bytes for paramaters

; these two must be the first thing reserved in zeropage
.segment "ZEROPAGE"
function_locals: .res FUNCTION_LOCAL_SIZE  
param:   .res FUNCTION_PARAMS_SIZE     

Anywhere else or in an included file define your callable functions as:
.func get_nametableaddress


.locals
 nametableaddress .byte
.endlocals

; IN:  reg x has nametable x, reg y has nametable y
; OUT : reg x has low address, reg y has high address
; LOCAL: Uses 1 byte

 tya     
 asl    
 
 asl    
 asl
 asl
 asl
  
 stx local::nametableaddress
 ora local::nametableaddress 
 tax
 tya 
 lsr
 lsr
 lsr
 ora #$20
 tay
 rts
 
.endfunc

If you wish to define a function as above inside a scope and export/import it:
;export:
.exportfunc get_nametableaddress

;import:
.importfunc get_nametableaddress
In code, you can use call:
  call get_nametableaddress, #10,#10
The macro will generate a small amount of code that checks the local variable usage and parameter usage. In this example no paramater memory is used because the macros justs loads reg x and y. There is no stack! Only a small amount of code will be output that checks for overuse or nested use of paramaters and local memory use and outputs a warning, so you can look closer and see if there is a problem or not. If you do pass paramaters in paramater memory space use clear_param num, where num is the number of paramaters acknolwedged as read/no longer needed:
.func some_function

; IN reg X,Y (addressIN) and a, param+0 (addressout)

.locals
 addressIN .word
 addressOUT .word
.endlocals



 stx local::addressIN
 sty local::addressIN + 1
 
 sta local::addressOUT
 lda param+0
 clear_param 1
 sta local::addressOUT + 1
 ;.....
If you look at the very first line of code:
FUNCTION_RANGE_CHECKING_ON = 1 ;Turn off if functions are okay
If you comment that line, no extra code will be generated and your assembled code will be the same as if you did not use any of these macros.

Code:

; Define this somewhere in your main module near the begining:
; FUNCTION_RANGE_CHECKING_ON = 1
; FUNCTION_LOCAL_SIZE = $08
; FUNCTION_PARAMS_SIZE = $08

;.segment "ZEROPAGE"
;function_locals: .res FUNCTION_LOCAL_SIZE
;param: .res FUNCTION_PARAMS_SIZE


.feature leading_dot_in_identifiers

.if ::FUNCTION_RANGE_CHECKING_ON
.pushseg
.segment "BSS"
locals_used: .res 1
params_used: .res 1
.popseg
.endif

.macro .exportfunc func1, func2, func3, func4, func5, func6, func7, func8, func9, func10

.export func1
.export .ident(.sprintf("%s%s",.string(func1),"___sizeof_locals" ))
.ifnblank func2
.exportfunc func2, func3, func4, func5, func6, func7, func8, func9, func10
.endif

.endmacro

.macro .importfunc func1, func2, func3, func4, func5, func6, func7, func8, func9, func10

.import func1
.import .ident(.sprintf("%s%s",.string(func1),"___sizeof_locals" ))
.ifnblank func2
.importfunc func2, func3, func4, func5, func6, func7, func8, func9, func10
.endif

.endmacro


.macro .func name

.ifdef _name_
.undefine _name_
.endif
.define _name_  name

.proc _name_

.endmacro


.macro .locals
_locals_ .set 1
.struct local

.endmacro

.macro .endlocals
.endstruct
.endmacro



.macro .endfunc
.endproc

.ifdef  _name_::_locals_
.ident(.concat(.string(_name_),"___sizeof_locals" )) = .sizeof(_name_::local)
.else
.ident(.concat(.string(_name_),"___sizeof_locals" ))  = 0
.endif

.endmacro


.macro call function, paramX, paramY, paramA, param0, param1, param2, param3

; use in order: reg.x, reg.y, reg.a, param zeropage, ....etc

.if .xmatch(paramA,a)
pha
.endif


.if ::FUNCTION_RANGE_CHECKING_ON
.ifdef  ::.ident(.concat(.string(function),"___sizeof_locals" ))

lda locals_used
if not zero
debugOut { "WARNING: Nested local variable usage calling: ", .string(function) }
endif
lda # ::.ident(.concat(.string(function), "___sizeof_locals"))
clc
adc locals_used
sta locals_used
cmp #FUNCTION_LOCAL_SIZE
if greater
debugOut { "WARNING: Function: '", .string(function), "' local memory exceeded: ",  fHex8 { locals_used } }
endif
.endif
.if .paramcount > 4
lda params_used
if not zero
debugOut { "WARNING: Nested paramater usage: ", .string(function) }
endif
lda #<( .paramcount - 4)
clc
adc params_used
sta params_used
cmp #FUNCTION_PARAMS_SIZE
if greater
debugOut { "WARNING: Function: '", .string(function), "' parameter memory exceeded: ", fHex8 { params_used } }
endif
.endif
.endif
.ifnblank param3
lda param3
sta param+3
.endif
.ifnblank param2
lda param2
sta param+2
.endif
.ifnblank param1
lda param1
sta param+1
.endif
.ifnblank param0
lda param0
sta param+0
.endif
.ifnblank paramA
.if .xmatch (paramA,a)
pla
.else
lda paramA
.endif
.endif
.ifnblank paramY
.if .not .xmatch (paramY,y)
ldy paramY
.endif
.endif
.ifnblank paramX  ; reg.x
.if .not .xmatch(paramX,x)
ldx paramX
.endif
.endif
jsr function
.if ::FUNCTION_RANGE_CHECKING_ON .and .defined(::.ident(.concat(.string(function), "___sizeof_locals")))
lda locals_used
sec
sbc # ::.ident(.concat(.string(function), "___sizeof_locals"))
sta locals_used
.endif
.endmacro


.macro clear_param num ; clear a vlaue from the param count
.if ::FUNCTION_RANGE_CHECKING_ON
.repeat num
dec params_used ; if range checking on, dec params
.endrepeat
.endif
.endmacro



No comments:

Post a Comment