Z80 MasterClass: Case Statement in the Assembly Language

A well-planned design can make your job a whole lot easier. Just take a look at how this case statement in assembly streamlines things, for instance.

        ld      a,(hl)                  
        cp      #' '
        jr      z,is_space
        cp      #'_'
        jr      z,is_underscore
        cp      #'$'
        jr      z,is_dollar
        jr      error_handler

The code is well-organized and straightforward. This is due to the fact that the compare instruction outputs the result in the Z flag while retaining the value of the A register. But what happens if you need to determine if a value falls within a specific range? Using the CP instruction would disrupt the natural flow, leading to jumps and increased complexity.

So here's a solution. Let's make our function act like the CP instruction, preserve the A register, and set or reset the Z flag as needed.

The function below does exactly that. It takes the symbol stored in the A register and checks if it falls within the bounds defined by the D and E registers (i.e. D >= A >= E). The result is then returned in the Z flag.

        ;; test if A is within DE: D >= A >= E
        ;; input(s):
        ;;   A    value to test (preserved!)
        ;;   DE   interval
        ;; output(s):
        ;;   Z    zero flag is 1 if inside, 0 if outside
        ;; affects:
        ;;   D, E, flags
test_inside_interval::
        push    bc                      ; store original bc
        ld      c,a                     ; store a
        cp      e                       ; a=a-e
        jr      nc, tidg_possible       ; a>=e       
        jr      tidg_false              ; false
tidg_possible:
        cp      d                       ; a=a-d
        jr      c,tidg_true             ; a<d
        jr      z,tidg_true             ; a=d
        jr      tidg_false
tidg_true:
        ;; set zero flag
        xor     a                       ; a=0, set zero flag
        ld      a,c                     ; restore a
        pop     bc                      ; restore bc
        ret
tidg_false:
        ;; reset zero flag
        xor     a
        cp      #0xff                   ; reset zero flag
        ld      a,c                     ; restore a
        pop     bc                      ; restore original bc
        ret

We can easily create more functions like this by simply filling in the DE and A registers and linking the calls together smoothly, just like in the test_if_digit function below.

        ;; test if A is a digit by reusing previous function
test_is_digit::
        ld      de,#0x3930              ; d='9', e='0'
        jr      test_inside_interval    ; ret optimization...

It's worth noting that this function does not end with the ret instruction. Instead, it jumps to the next function. And once that next function returns, it will proceed straight to the callee, saving us one jump.

By using this technique, we can optimize our code. The following code snippet implements several additional functions, all utilizing the same ret instruction.

test_is_alpha::
        call    test_is_upper
        ret     z
        jr      test_is_lower

test_is_upper::
        ld      de,#0x5a41              ; d='Z'. e='A'
        jr      test_inside_interval

test_is_lower::
        ld      de,#0x7a61              ; d='z', e='a'
        jr      test_inside_interval    ; last tests' result is the end result

test_is_alphanumeric::
        call    test_is_digit
        ret     z
        jr      test_is_alpha

In conclusion, if your comparison functions mimic the CP instruction, you can link them together and write sophisticated case statements that seamlessly integrate with the assembly language.

        ld      a,(hl)              
        call    test_is_digit           
        jr      z,is_digit
        cp      #' '
        jr      z,is_space
        cp      #'_'
        jr      z,is_underscore
        call    test_is_alpha
        jr      z,is_alpha
        jr      error_handler

For a practical demonstration of this approach, check out this real-world example - it's a partially implemented version of the ctype.h standard C library functions.