* * * * *

                Unit testing from inside an assembler, part II

I started working on unit tests from inside the assembler [1]. I'm not sure
how MOS [2] does it (as I don't read Rust [3]) so I'm making this up as I go
along. I'm using the following file as a test case for the work:

-----[ Assembly ]-----
lfsr            equ     $F6

               org     $4000
start           bsr     random
               rts

the.byte        fcb     $55
the.word        fdb     $AAAA

;***********************************************
;       RANDOM          Generate a random number
;Entry: none
;Exit:  B - random number (1 - 255)
;***********************************************

random          ldb     lfsr
               andb    #1
               negb
               andb    #$B4
               stb     ,-s
               ldb     lfsr
               lsrb
               eorb    ,s+
               stb     lfsr
               rts

       .test   "random"
               ldx     #.result_array
               clra
               clrb
setmem          sta     ,x+
               decb
               bne     .setmem
               ldx     #.result_array + 128
               lda     #1
               sta     lfsr
               lda     #255
       .tron
loop            bsr     random
       .assert /B <> 0 , "degenerate LFSR"
               tst     b,x
       .assert /CC.z <> 1 , "non-repeating"
       .troff
               inc     b,x
               deca
               bne     .loop
       .assert @the.byte == $55 && @@the.word == $AAAA , "tis a silly test"
               rts
result_array    rmb     256

       .endtst

               nop

;***********************************************

               end     start
-----[ END OF LINE ]-----

I've made the “unit test” … thing, a backend (like I have for binary and
Color Computer-specific output as backends) because it's less intrusive on
the code and I wasn't sure where to assemble the test code (within the memory
space of the 6809). By making this a specific backend, it should be apparent
that this is not for the final version of the code.

So far, I have it such that all the non-test backends don't see the code at
all:

-----[ Listing ]-----
                        | FILE test.asm
                      1 |
                      2 | lfsr            equ     $F6
                      3 |
                      4 |                 org     $4000
4000: 8D    04         5 | start           bsr     random
4002: 39               6 |                 rts
                      7 |
4003: 55               8 | the.byte        fcb     $55
4004: AAAA             9 | the.word        fdb     $AAAA
                     10 |
                     11 | ;***********************************************
                     12 | ;       RANDOM          Generate a random number
                     13 | ;Entry: none
                     14 | ;Exit:  B - random number (1 - 255)
                     15 | ;***********************************************
                     16 |
4006: D6    F6        17 | random          ldb     lfsr
4008: C4    01        18 |                 andb    #1
400A: 50              19 |                 negb
400B: C4    B4        20 |                 andb    #$B4
400D: E7    E2        21 |                 stb     ,-s
400F: D6    F6        22 |                 ldb     lfsr
4011: 54              23 |                 lsrb
4012: E8    E0        24 |                 eorb    ,s+
4014: D7    F6        25 |                 stb     lfsr
4016: 39              26 |                 rts
                     27 |
                     28 |         .test   "random"
                     29 |                 ldx     #.result_array
                     30 |                 clra
                     31 |                 clrb
                     32 | .setmem         sta     ,x+
                     33 |                 decb
                     34 |                 bne     .setmem
                     35 |                 ldx     #.result_array + 128
                     36 |                 lda     #1
                     37 |                 sta     lfsr
                     38 |                 lda     #255
                     39 |         .tron
                     40 | .loop           bsr     random
                     41 |         .assert /B <> 0 , "degenerate LFSR"
                     42 |                 tst     b,x
                     43 |         .assert /CC.z <> 1 , "non-repeating"
                     44 |         .troff
                     45 |                 inc     b,x
                     46 |                 deca
                     47 |                 bne     .loop
                     48 |         .assert @the.byte == $55 && @@the.word == $AAAA , "tis a silly test"
                     49 |                 rts
                     50 | .result_array   rmb     256
                     51 |
                     52 |         .endtst
                     52 |         .endtst
                     53 |
4017: 12              54 |                 nop
                     55 |
                     56 | ;***********************************************
                     57 |
                     58 |                 end     start

   2 | equate      00F6     3 lfsr
  17 | address     4006     1 random
   5 | address     4000     1 start
-----[ END OF LINE ]-----

Ignore that line 52 shows up twice here—that's a bug that I'll work on (my
initial fix removed the duplicate line, but line 51 didn't show up—it's not a
show-stopping bug which I why it's going on the “fix it later” list). Also,
the labels the.byte and the.word don't show up on the symbol list at the end
due to a “feature” where labels that aren't referenced aren't printed (that
was to remove unused equates from the symbol list). So for the non-test
backends, the actual testcase isn't part of the build.

The other added directives, like .tron, .troff and .assert are also ignored
by the other backends if the directives appear outside a “unit test.”

With the .test backend though, all the directives are recognized and most of
them work, although I'm still working on .assert (see below).

One issue—when to run the actual tests. Right now, the code is run when then
endtst directive is hit, as running the code as it's assembled won't work
well I think, especially with branches and calls to other routines, and it
would be a nightmare to get correct. It's easier if all the code exists in
“memory,” but one issue I've noticed is that any code further down in the
file can't be used. I'll have to move the execution of tests to after the
assembly pass is done.

The .tron and .troff directives work, dumping out the instructions between
them as the code is run:

-----[ data ]-----
.. lots of lines cut
PC=402A X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=09 B=D2 CC=-f-i---c | 402A 8D   DA     - BSR   4006               ; ----- backwards
PC=402C X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=09 B=69 CC=-f-i---- | 402C 6D   85     - TST   B,X                ; -aa0- 411D = 00
PC=402A X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=08 B=69 CC=-f-i---- | 402A 8D   DA     - BSR   4006               ; ----- backwards
PC=402C X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=08 B=80 CC=-f-in--c | 402C 6D   85     - TST   B,X                ; -aa0- 4034 = 00
.. more lines cut
-----[ END OF LINE ]-----

Another issue is dealing with the .assert directive. I have to save the test
somehow since the assembler can't do the check when it parses the .assert
because not all the code for the test has been assembled yet. I could store
the text to the test expression and then evaluate it at run time, but as this
code shows, that would mean re-interpreting the text many, many times. No,
the solution I came up with is a mini-Forth-like language for evaluating the
test expression.

Yup, I'm embedding a mini-Forth interpreter in a 6809 assembler written in C.

A classic blunder I'm sure, like getting involved in a land war in Asia, or
going against a Sicilian when death is on the line, but I'm not sure of any
other way. The mini-Forth is very small though, only 41 words are defined,
but it's enough for my needs. The first .assert expression translates to:

-----[ miniForth ]-----
VM_CPUB         ( push contents of the B register onto the stack )
VM_LIT 0        ( push a literal 0 onto the stack )
VM_NE           ( compare the two, leaving a flag on the stack )
VM_EXIT         ( exit the VM )
-----[ END OF LINE ]-----

The second one to:

-----[ miniForth ]-----
VM_CPUCCz       ( push the CC zero flag )
VM_LIT 0        ( push a literal 0 )
VM_NE           ( compare the two, leave flag on stack )
VM_EXIT         ( exit the VM )
-----[ END OF LINE ]-----

And the last one to:

-----[ miniForth ]-----
VM_LIT 0x4003   ( push the literal 0x4003 )
VM_AT8          ( fetch the byte from the 6809 memory buffer )
VM_LIT 0x55     ( push the literal 0x55 )
VM_EQ           ( compare the two, leave flag on stack )
VM_LIT 0x4004   ( push the literal 0x4004 )
VM_AT16         ( fetch two bytes from the 6809 memory buffer )
VM_LIT 0xAAAA   ( push the literal 0xAAAA )
VM_EQ           ( compare the two, leave flag on stack )
VM_LAND         ( AND the two results, leaving flag on stack )
VM_EXIT         ( exit the VM )
-----[ END OF LINE ]-----

This works, and it was easy to implement the VM (Virtual Machine). Now all I
have to do is parse the expression to assemble the VM code (right now the
addresses and VM functions are hard coded into the assembler just to prove it
works).

This feature is proving to be an interesting problem.

[1] gopher://gopher.conman.org/0Phlog:2023/11/29.2
[2] https://mos.datatra.sh/guide/unit-testing.html
[3] https://github.com/datatrash/mos

Email author at [email protected]