Statements are SpectNetIDE specific control flow constructs — thanks again for the inspiration by Simon Brattel — that instruct the compiler about loop-like and conditional compilation.
While directives help you to organize your code and include code files optionally according to the compilation context, statements provide you more useful tools to shorten the way you can declare Z80 assembly code.
Each statement can be written with a leading dot, or without it, and the compiler accepts both lowercase
and uppercase versions. For example all of these version are valid: .if, if, .IF, and IF.
The LOOP Block
With LOOP block, you can organize a cycle to emit code. Here is a sample that tells the gist:
.loop 6
add hl,hl
.endl
This is a shorter way to multiply HL with 64. It is equivalent with the following code:
add hl,hl
add hl,hl
add hl,hl
add hl,hl
add hl,hl
add hl,hl
The .loop statement accepts an expression. The compiler repeats the instructions within the
loop’s body according to the value of the expression. The .endl statement marks the end of the loop.
You can use many flavors for the
.endlblock closing statement..endl,endl,.lend,lendare all accepted — with fully uppercase letters, too.
Look at this code:
counter .equ 2
; do something (code omitted)
.loop counter + 1
.db #80, #00
.endl
This is as if you wrote this:
.db #80, #00
.db #80, #00
.db #80, #00
The LOOP Scope
The .loop statement declares a scope for all labels, symbols, and variables declared in the loop’s
body. Every iteration has its separate local scope. When the assembler resolves symbols, it starts
from the scope of the loop, and tries to resolve the value of a symbol. If it fails, steps out to
the outer scope, and goes on with the resolution.
Check this code:
value .equ 2
; do something (code omitted)
.loop 2
value .equ 5
ld a,value
.endl
The compiler takes it into account as if you wrote this:
ld a,5
ld a,5
The value symbol declared within the loop, overrides value in the outer scope, and
thus 5 is used instead of 5.
Nonetheless, you when you utilize a different construct, it seems a bit strange at first:
value .equ 2
; do something (code omitted)
.loop 2
ld a,value
value .equ 5
ld b,value
.endl
The strangeness is that the compiler creates this:
ld a,2
ld b,5
ld a,2
ld b,5
When the assembler resolves value in the ld a,value instruction, if finds value
in the outer scope only, as it is not declared yet within the loop’s scope. In the ld b,value
instruction value gets resolved from the inner scope, so it takes 5.
Variables and Scopes
Unlike symbols that work as constant values, variables (declared with the .var pragma, or its syntactical
equivalents, the = or := tokens) can change their values.
Take a look at this code:
counter = 4
.loop 3
innercounter = 4
ld a,counter + innercounter
counter = counter + 1
.endl
Here, the counter variable is defined in the global scope (out of the loop’s scope), while innercounter in
the local scope of the loop. When evaluating the counter = counter + 1 statement, the compiler finds counter in
the outer scope, so it uses that variable to increment its value. This code emits machine code for this source:
ld a,#08
ld a,#09
ld a,#0A
Now, add a single line to the loop’s code:
counter = 4
.loop 3
innercounter = 4
ld a,counter + innercounter
counter = counter + 1
.endl
ld b,innercounter
The compiler will not compile this code, as it cannot find the value for innercounter in the ld b,innercount
instruction. Because innercounter is defined in the local scope of the loop, this scope is immediately disposed as
the loop is completed. When the compiler processes the ld b,innercounter instruction, the local scope is not
available.
Labels and Scopes
Labels behave like symbols, and they work similarly. When you create a label within a loop, that label is created in the local scope of the loop. The following code helps you understand which labels are the part of the global scope, and which are created in the loop’s scope:
.org #8000
MyLoop: .loop 2
ld bc,MyLoop
Inner:
ld de,MyEnd
ld hl,Inner
ld ix,Outer
MyEnd: .endl
Outer: nop
The label of the .loop statement is part of the outer (global) scope, just like the label that follows the
.endl statement. However, all labels declared within the loop’s body, including the label of the .endl
statement belongs to the local scope of the loop.
Thus, the compiler translates the code above into this one:
(#8000): ld bc,#8000 (MyLoop)
Inner_1 (#8003): ld de,#800D (MyEnd_1)
(#8006): ld hl,#8003 (Inner_1)
(#8009): ld ix,#801A (Outer)
MyEnd_1 (#800D): ld bc,#8000 (MyLoop)
Inner_2 (#8010): ld de,#801A (MyEnd_2)
(#8013): ld hl,#8010 (Inner_2)
(#8016): ld ix,#801A (Outer)
MyEnd_2
Outer (#801A): nop
Here, Inner_1, Inner_2, MyEnd_1, and MyEnd_2 represents the labels created in the local scope of the
loop. The _1 and _2 suffixes indicate that each loop iteration has a separate local scope. As you can see,
the last iteration of MyLabel points to the first outer address (Outer label).
Nesting LOOPs
Of course, you can nest loops, such as in this code:
.loop 3
nop
.loop 2
ld a,b
.endl
inc b
.endl
This code snippet translates to this:
nop
ld a,b
ld a,b
inc b
nop
ld a,b
ld a,b
inc b
nop
ld a,b
ld a,b
inc b
When you nest loops, each loop has its separate scope.
The $CNT value
It is very useful to use the $cnt value that represents the current loop counter. It starts from
1 and increments to the maximum number of loops. This sample demonstrates how you can use it:
.loop 2
outerCount = $cnt
.loop 3
.db #10 * outerCount + $cnt
.endl
.endl
This code translates to this:
.db #11
.db #12
.db #13
.db #21
.db #22
.db #23
You can observe that each loop has its spearate $cnt value.
The
$ctnvalue has several syntax versions that the compiler accepts:$CNT,.cnt, and.CNT.
The PROC..ENDP Block
In the previous section you could understand how labels and scopes work for the .loop statement.
You can utilize this scoping mechanism with the help of the .proc...endp statement.
This sample code demonstrates the concepts (just as you learned earlier):
.org #8000
MyLabel:
ld de,Outer
ld hl,Mylabel
call MyProc
halt
MyProc:
.proc
ld bc,MyProc
MyLabel:
ld de,MyEnd
ld hl,MyLabel
ld ix,Outer
ret
MyEnd:
.endp
Outer: nop
The first MyLabel label belongs to the global scope, while the second (within MyProc)
to the local scope of the procedure wrapped between .proc and endp. MyProc belongs to the
global scope too, however, MyEnd is the part of the MyProc scope, so it is visible only from
within the procedure.
The assembler emits this code:
MyLabel (#8000): ld de,#8018 (Outer)
(#8003): ld hl,#8000 (MyLabel)
(#8006): call #800A (MyProc)
(#8009): halt
MyProc (#800A): ld bc,#800A (MyProc)
MyLabel_ (#800D): ld de,#8018 (MyEnd)
(#8010): ld hl,#800D (MyLabel_)
(#8013): ld ix,#8018 (Outer)
(#8017): ret
MyEnd
Outer (#8018): nop
You can nest PROC bloks just as LOOP blocks. Each PROC block has its private scope.
When the compiler sees a PROC block, it works just as if you wrote .loop 1.
NOTE:
PROCis different than a loop. You cannot use the$cntvalue. Similarly, thebreakandcontinueinstructions are unavailable within aPROCblock.
The assembler accepts these aliases for
PROCandENDP:.proc,proc,.PROC,PROC,.endp,.ENDP,endp,ENDP,.pend,.PEND,pend,PEND.
The REPEAT..UNTIL Block
While the .loop statement works with an expression that specified the loop counter,
the .repeat...until block uses an exit condition to create more flexible loops.
Here is a sample:
counter = 0
.repeat
.db counter
counter = counter + 3
.until counter % 7 == 0
Observe, the counter % 7 == 0 condition specifies when to exit the loop. Because the
exit condition is examined only at the end of the loop, the .repeat blocks executes
at least once.
The sample above translates to this:
.db 0
.db 3
.db 6
.db 9
.db 12
.db 15
.db 18
The .repeat block uses the same approach to handle its local scope, symbols, labels, and
variables as the .loop block. The block also provides the $cnt loop counter that starts
from 1 and increments in every loop cycle.
This sample demontrates the .repeat block in action:
.org #8000
counter = 0
.repeat
.db low(EndLabel), high(Endlabel), $cnt
counter = counter + 3
EndLabel: .until counter % 7 == 0
The compiler translates the code to this:
.db #03, #80, #01
.db #06, #80, #02
.db #09, #80, #03
.db #0C, #80, #04
.db #0F, #80, #05
.db #12, #80, #06
.db #15, #80, #07
The WHILE..ENDW Block
With .while loop, you can create another kind of block, which uses entry condition. For example,
the following code snippet generates instructions to create the sum of numbers from 1 to 9:
counter = 1
ld a,0
.while counter < 10
add a,counter
counter = counter + 1
.endw
The .while...endw block uses an entry condition declared in the .while statement. Provided, this
condition is true, the compiler enters into the body of the loop, and compiles all instructions and statements
until it reaches the .endw statement. Observe, it may happen that the body of the loop is never reached.
The compiler translates the code snippet above to the following:
ld a,0
add a,1
add a,2
add a,3
add a,4
add a,5
add a,6
add a,7
add a,8
add a,9
Just like the .loop and the .repeat blocks, .while uses the same approach to handle its local scope,
symbols, labels, and variables. This block also provides the $cnt loop counter that starts from 1 and increments
in every loop cycle.
This code demonstrates the .while block with labels and using $cnt value:
counter = 0
.while counter < 21
.db low(EndLabel), high(Endlabel), $cnt
counter = counter + 3
EndLabel: .endw
The compiler translates the code to this:
.db #03, #80, #01
.db #06, #80, #02
.db #09, #80, #03
.db #0C, #80, #04
.db #0F, #80, #05
.db #12, #80, #06
.db #15, #80, #07
You can use many flavors for the
.endwblock closing statement..endw,endw,.wend,wendare all accepted — with fully uppercase letters, too.
The FOR..NEXT Loop
Tou can use the traditional .for...next loop to create a loop:
.for myVar = 2 .to 5
.db 1 << int(myVar)
.next
This loop uses the myVar variable as its iteration variable, which iterates from 1 to 4. As you expect,
the compiler translates the for-loop into this:
.db #04
.db #08
.db #10
.db #20
You can specify a .step close to change the loop increment value:
.for myVar = 1 .to 7 .step 2
.db 1 << int(myVar)
.next
Now, the code translates to this:
.db #02
.db #08
.db #20
.db #80
You can create a loop with decrementing iteration variable value:
.for myVar = 7 .to 1 .step -2
.db 1 << int(myVar)
.next
As you expect, now you get this translation:
.db #80
.db #20
.db #08
.db #02
Just as with the other statements, you can use the
.for,.to, and.stepkeywords without the.prefix, sofor,to, andstepare also valid.
The for-loop can do the same stunts as the other kind of loops; it handles labels, symbols, and variables exactly the same way. There’s only one exception, the loop iteration variable. If this variable is found in an outer scope, instead of using that value, the compiler raises an error. You can us the for-loop only with a freshly created variable.
So both cases in this code raise an error:
myVar = 0
.for myVar = 1 .to 4 ; ERROR: Variable myVar is already declared
; ...
.next
.for _i = 1 .to 3
.for _i = 3 .to 8 ; ; ERROR: Variable _i is already declared
; ...
.next
.next
As
iis a reserved token (it represents theIregister), you cannot useias a variable name. Nonetheless,_iis a valid variable name.
The for-loop works with both integer and float variables. If any of the initial value, the last value (the one after .to),
or the increment value (the one after .step) is a float value, the for-loop uses float operations; otherwise it uses
integer operations.
This code snippet demonstrates the difference:
.for myVar = 1 .to 4 .step 1
.db 1 << myVar
.next
.for myVar = 1 .to 4 .step 1.4
.db 1 << myVar ; ERROR: Right operand of the shift left operator must be integral
.next
Nonetheless, you can solve this issue with applying the int() function:
.for myVar = 1 .to 4 .step 1.4
.db 1 << int(myVar) ; Now, it's OK.
.next
You can still use the
$cntvalue in for loops. Just like with other loop, it indicates the count of cycles strating from one and incremented by one in each iteration.
Maximum Loop Count
It’s pretty easy to create an infinite (or at least a very long) loop. For example, these loops are obviously infinite ones:
.repeat
.until false
.while true
.wend
The assembler checks the loop counter during compilation. Whenever it exceeds #FFFF (65535), it raises an error.
The IF..ELIF..ELSE..ENDIF Statement
You can use the .if statement to create branches with conditions. For example, this code emits inc b
or inc c statement depending on whether the value of branch is even or odd:
.if branch % 2 == 0
inc b
.else
inc c
.endif
You do not have to specify an .else branch, so this statement is entirely valid:
.if branch % 2 == 0
inc b
.endif
You can nest if statements like this to manage four different code branches according to the value of branch:
.if branch == 1
inc b
.else
.if branch == 2
inc c
.else
.if branch == 3
inc d
.else
inc e
.endif
.endif
.endif
Nonetheless, you can use the .elif statement to create the code snippet above in clearer way:
.if branch == 1
inc b
.elif branch == 2
inc c
.elif branch == 3
inc d
.else
inc e
.endif
IF and Scopes
Unlike the loop statements, .if does not provide its local scope. Whenever you create a symbol, a label or
a variable, those get into the current scope. This code defines a label with the same name in each branches. Because
the compiler evaluates the .if branches from top to down, it either compiles one of the .elif branches —
the first with a matching condition — or the else branch. Thus, this code does not define MyLabel twice:
branch = 4 ; Try to set up a different value
; Do something (omitted from code)
ld hl,MyLabel
.if branch == 1
inc b
MyLabel ld a,20
.elif branch > 2
MyLabel ld a,30
inc c
.elif branch < 6
inc d
MyLabel ld a,40
.else
MyLabel ld a,50
inc e
.endif
Generally, you can decorate any statement with labels. The .elif and .else statements are exception. If you
do so, the compiler raises an error:
.if branch == 1
inc b
MyLabel ld a,20
.elif branch > 2
MyLabel ld a,30
inc c
Other .elif branch < 6 ; ERROR: ELIF section cannot have a label
inc d
MyLabel ld a,40
Another .else ; ERROR: ELSE section cannot have a label
MyLabel ld a,50
inc e
.endif
IF Nesting
When you nest .if statements, take care that each of them has a corresponding .endif. Whenever
the compiler finds an .endif, is associates it with the closest .if statement before .endif.
I suggest you use indentation to make the structure more straightforward, as the following code snippet shows:
row = 2
col = 2
; Change row and col (omitted from code)
.if row == 0
.if col == 0
.db #00
.elif col == 1
.db #01
.else
.db #02
.endif
.elif row == 1
.if col == 0
.db #03
.elif col == 1
.db #04
.else
.db #05
.endif
.elif row == 2
.if col == 0
.db #06
.elif col == 1
.db #07
.else
.db #08
.endif
.else
.if col == 0
.db #09
.elif col == 1
.db #0A
.else
.db #0B
.endif
.endif
The IFUSED/IFNUSED Statements
SpectNetIDE offers a similar construct to IF..ELIF..ELSE..ENDIF, using the IFUSED or IFNUSED statement instead of IF. These new statements are specialized forms of IF. You can use these statements to emit code depending on whether a symbol (label, .EQU, .VAR, structure, or structure field) exists and has already been used by the code preceding the IFUSED/IFNUSED statement.
Here are a few examples:
MyProc:
ld hl,#5800
ld (hl),a
ret
; some other code
.ifused MyProc
MyMsg: .defn "MyProc is used"
.else
MyMsg: .defn "MyProc is not used"
.endif
Main:
ld hl,MyMsg
Here, the .ifused statement will set the string the MyMsg label point to according to whether the MyProc label is used, or not. As in this case MyProc is defined but not invoked before the .ifused statement, HL will point to the “MyProc is not used” message.
Should you call MyProc before .ifused, HL would point to the other message, “MyProc is used”:
MyProc:
ld hl,#5800
ld (hl),a
ret
; some other code
call MyProc
; some other code
.ifused MyProc
MyMsg: .defn "MyProc is used"
.else
MyMsg: .defn "MyProc is not used"
.endif
Main:
ld hl,MyMsg
The .ifnused statement is the complement of .ifused. It is evaluated to a true condition value only if the symbol following .ifnused is not defined, or, if defined, is not used.
IFUSED/IFNUSED Syntax
You need to specify a symbol after the .ifused or .ifnused keywords. These symbols must follow the syntax of identifiers. They can be compound names used for modules and structures. So, all of these symbol names are correct:
MyLabel
MyStruct
MyStruct.FieldX
MyModule.Main
::NestedModule.Start.MyProc
Note: You can use these aliases for
.ifused:.IFUSED,ifused,IFUSED. Similarly,.ifnusedaccept alternative tokens:.IFNUSED,ifnused,IFNUSED.
IFUSED/IFNUSED Semantics
The SpectNetIDE compiler accepts any .ifused and .ifnused statements until they are syntactically correct. When the assembler tests their condition, it works this way:
- If the specified symbol does not exists,
.ifusedevaluates to false, while.ifnusedevaluates to true. - If the particular symbol exists and it is used in the code section preceding the
.ifusedor.ifnusedstatement,.ifusedevaluates to true,.ifnusedto false. - If the particular symbol exists and it is not used in the code section preceding the
.ifusedor.ifnusedstatement,.ifusedevaluates to false,.ifnusedto true.
These statements do not support look-ahead in the code. This behavior could lead to paradox situations, like in this example:
MyFlag = true
MyValue: .equ #1234
; some other code that does not use MyValue
.ifused MyValue
MyFlag = false;
.endif
; some other code that does not change MyFlag
.if MyFlag
ld a,MyValue
.endif
Should .ifused work with look-ahead, this code would make the compiler scratch its virtual head. Because MyFlag is set to true, the .if statement at the bottom of the code would emit an ld a,MyValue instruction. Knowing this fact, the compiler would say that .ifused MyValue should be evaluated to true. However, in this case, the body .ifused would set MyFlag to true, and that would prevent the bottom .if to emit ld a,MyValue, and then MyValue would not be used at all.
Block Statements without a Closing Statement
The compiler automatically recognizes if a block does not have a closing statement, and provides an error message accordingly.
Orphan Closing Statements
When the compiler finds a closing statement (such as .endw, .endl, .until, .endif, etc.) it will
issue an error.
The BREAK statement
You can exit the loop — independently of the loop’s exit condition — with the .break statement:
; LOOP sample
.loop 5
.if $cnt == 4
.break
.endif
.db $cnt
.endl
; REPEAT sample
.repeat
.if $cnt == 4
.break
.endif
.db $cnt
.until $cnt == 5
; WHILE sample
.while $cnt < 5
.if $cnt == 4
.break
.endif
.db $cnt
.endw
; FOR-loop sample
.for value = 1 to 5
.if value == 4
.break
.endif
.db value
.next
Because all these loops are exited at the beginning of the 4th iteration, they produce this output:
.db #01
.db #02
.db #03
You cannot use the
.breakstatement outside of a loop construct. If you do so, the compiler raises an error.
The CONTINUE Statement
You can interrupt the current iteration of the loop and carry on the next iteration with the .continue statement:
; LOOP sample
.loop 5
.if $cnt == 4
.continue
.endif
.db $cnt
.endl
; REPEAT sample
.repeat
.if $cnt == 4
.continue
.endif
.db $cnt
.until $cnt == 5
; WHILE sample
.while $cnt <= 5
.if $cnt == 4
.continue
.endif
.db $cnt
.endw
; FOR-loop sample
.for value = 1 to 5
.if value == 4
.continue
.endif
.db value
.next
Because all these loops skip the 4th iteration, they produce this output:
.db #01
.db #02
.db #03
; #04 is skipped
.db #05
You cannot use the
.continuestatement outside of a loop construct. If you do so, the compiler raises an error.