Clean z80 Coding (2/3)

by Hicks.

If you have read the first part of this article, your code is now readable (#1), no hardcoded (#2), full of macros (#3) and well commented (#4)? We can therefore discuss other equally important rules: meaningful names (#5), conditional code (#6), and assertions (#7).

Rule #5: Choose Meaningful Names

When assemblers such as DAMS only allowed 8 characters per label, we were forced to use perilous name contractions, almost unreadable afterwards. For a project in progress, this is not too embarrassing, but if it grows up, if you come back to it after months, or worse, if it is a collaborative project on which several people are working, misnaming labels is the assurance of getting confused and not being able to find your way around.

The most general rule for naming labels is to give meaningful names, which reveal the intention behind what is being named. It is therefore necessary to give explicit names, so avoid the implicit as much as possible. For example, it is impossible to understand what is behind the variable ‘n’, or the routine ‘toto’ (hello TotO!).

Sometimes, an attempt is made to make a name explicit with a comment as follows:

n = 10 ; level number

But this is a bad solution because when ‘n’ appears in a routine, what this symbol represents will remain completely obscure, and it will be much harder to understand the intent of the routine. A longer but more meaningful name is better (making the comment unnecessary):

level_number = 10

An essential point concerns the nomenclature. When names become long, it is essential to insert separators in the string, in order to highlight its structure, its levels and sub-levels. The snake case (‘level_number’) and camel case (‘levelNumber’) are the most common. Writing ‘levelnumber’ is not readable.

In any case, a name will only be meaningful if it corresponds to what it stands for. Two cases may arise, in general :

  1. If it is the name of a routine or a macro: start with a verb or verbal group, which allows you to signify an intention, what you want to do (‘clear_screen’).
  2. If it is the name of a symbol or variable: start with a noun or nominal group (often a noun and adjectives), not a verb. For example, ‘main_score’, ‘level_number’, etc. The noun ‘level’ alone is not enough meaningful because you may want to refer to the level number, its difficulty, etc. The noun must be qualified with an adjective or something similar.

Having meaningful names therefore generally implies:

  • Memorable and accessible names: should be easy to remember, to search for and then jump to easily (if editor has auto-completion, this makes it easier for you).
  • Differentiated names: avoid names that are too similar (especially if they are long), it is confusing and harder to look for.
  • Specific names: never use too generic names like ‘display’ as you may get ‘display_score’ further down the line. The ‘display’ routine doesn’t have a name that is explicit enough, we don’t know what it displays.
  • Make consistent choices (Madram will be grateful to you):
    1. Use only one word per concept and stick to it, otherwise it will confuse the code. For example, everything related to display in your code should be named ‘disp’, ‘display’, ‘aff’, etc., but you cannot mix two of these names to refer to the same concept.
    2. Use accurate typography. For example, put all constants or parameters in UPPERCASE (or screaming camel case, like ‘LEVEL_NUMBER’).
    3. Use the same prefixes and suffixes. For example, “_table” for all tables.

Another good habit is to use local labels, whose meaning can be only local (available since FF Orgams version, take a look here). In this case, it will be much more readable to write:

display_font
   ld b,8
.loop_y
   ...
   djnz .loop_y

...

display_logo
   ld b,24
.loop_y         ; no confusion with the preview one
   ...
   djnz .loop_y

Rule #6: Conditional Assembly

An assembly is conditional when certain portions of code are assembled only under certain conditions, explicitly specified by the programmer. This implies the use of the IF, ELSE, END (or equivalent) directive set.

This is often done by defining a series of constants at the beginning of the source that determine how the code you want to run should be assembled. The main advantage of such an approach is to gain flexibility, since by modifying a single value at the beginning of the source, one can move from one version of the code to another in an instantaneous way. To approach this principle, a bad but widespread habit is to comment out whole sections of code, which then have to be uncommented, which exposes you to the risk of errors. If your friends do this, don’t talk to them.

Let’s take this opportunity to underline a particularity of Orgams. While cross-assemblers like Rasm allow to manage conditions in a very flexible way (IF condition < 4 ELSE …), the current version of Orgams (June 22, 2021 at 10:20 pm) does not yet manage operators (<, =, >) and it is thus the value of the symbol itself which acts as condition (IF condition ELSE …). If this symbol is equal to zero, we jump to ELSE, otherwise the condition is fulfilled, whatever the value of ‘condition’. However, an alternative is proposed by Mdrm:

IF condition - 1 : ELSE
   ...   ; assembled if condition=1
END
IF condition - 2 : ELSE
   ...   ; assembled if condition=2
END
... 

Now, some examples.

Sometimes we like to place a flag to enable or disable the music in our production. So we simply end up with:

music = 1        ; 0: music off , 1: music on
...

   IF music
       ...      ; Play the music
   ELSE
       ...      ; Wait some cycles, or do nothing (in this case, remove 'ELSE')
   END

In the same way, it is useful to have a flag allowing you to switch between the working version of a code and its version for distribution: thus, by setting a single flag, you eliminate unnecessary initialisations, the key test for the return to the assembler, valuable ASSERTs (see rule #7), rasters which could allow you to visualise machine time, etc.

Concerning this last point, instead of writing:

view_cpu_flag = 1
...

   IF view_cpu_flag
      view_cpu(&10,76) ; border ink (&10) and color (76 = red)
   END

With:

MACRO view_cpu reg,val
   ld bc,&7f00+reg
   out (c),c
   ld c,val
   out (c),c
ENDM

It is preferable to combine conditional assembly and macros because it is often more advantageous to insert the condition in rather than out of the macros. It is better to write:

   view_cpu(&10,76) ; border ink (&10) and color (76 = red)

With:

MACRO view_cpu reg,val
   IF view_cpu_flag
      ld bc,&7f00+reg
      out (c),c
      ld c,val
      out (c),c
   END
ENDM

This makes the code as a whole more readable.

Another interesting use: in the context of 4k code, we like to be able to test how much space this or that variant of the code takes up, to see how it compresses, etc. The conditional code then becomes indispensable for assembling different versions of a code with equivalent results, in order to estimate its final size.

Rule #7: Assertions

It often happens that even with very clean coding, bugs can creep into the program. But there is also a clean way to deal with these bugs: placing assertions whose function is to detect abnormal operation of the program, and to report it to the developer as soon as possible. The earlier abnormal behaviour is detected, the easier it will be to remedy the cause.

Two types of assertions are generally distinguished:

  1. Assemble-time assertions: when assembling the code, they interrupt the assembly and signal this or that behavior that violates the rules we have established, via a small message.
  2. Run-time assertions: once the code is assembled, it is appreciated that the abnormal behaviors are also intercepted during the execution of the code.

The second type of assertions has already been presented by Golem in an article by our friends at Amstrad.info (french only). In short, it consists in activating small pieces of code which, if they detect an abnormal behavior, freeze the execution of the code in order to stay as close as possible to the source of the bug. I won’t come back to this because the article is quite complete, with clear examples. The objective is to be able to trace the bug from the breakpoint to the bug, in particular via the stack, the contents of the registers, etc.

So we can look at assertions that detect abnormal behavior at assembly time, and return a controlled error.

Under Orgams, there is no ASSERT directive, but it can be simulated through a macro, as indicated in the manual. An example sponsored by Madram: let’s imagine that a table needs to be aligned to an address multiple of &200. If we just write ‘table = &4000’ without any further precision, we might forget this constraint. A comment here only informs the reader, but does not impose any constraint on the overall structure.

It is therefore sufficient to declare two macros beforehand:

MACRO ASSERT predicate
    IF predicate:ELSE
       !! ERROR !!
    END
ENDM

MACRO ASSERT_NOT predicate
    IF predicate
       !! ERROR !!
    END
ENDM

The goal here is to ask Orgams to assemble a non-compliant code (!! ERROR !!) so that it stops and causes an error if an assertion does not return the expected result. We can then make our declarations as usual, followed by a test:

lz77_buffer = &8000
ASSERT_NOT(lz77_buffer AND &1FF)

The ideal is to reserve an area of our code for assertions, which will check that everything is going as planned. In our example, if the address of ‘lz77_buffer’ is not a multiple of &200, then the parameter passed to the ASSERT_NOT macro is not zero, so there is an error at assembly.

Conclusion?

One of the main obstacles to clean code is psychological: it often requires writing more lines, and adding parts of code that you think you can do without because they don’t change the program that is running. We often think, wrongly, that what is clean is to code in a compact way, with few lines and little optional code.

But this is a mistake: such code is often opaque, unreadable, and its stability depends only on your memory. I believe that many projects do not progress or are abandoned because they have not been coded properly: it is a pain to get back to it, or it is so badly coded that it becomes impossible to change anything without crashing everything, and without understanding anything!

We could continue this series with other “rules”, and that’s probably what will happen if I gather new ones! In the meantime, Eliot tells you: “stop talking, start clean coding”!

[Updated on July 11, 2021]