Friday, Debtember 01, 2023
Unit testing from inside an assembler, part II
I started working on unit tests from inside the assembler. I'm not sure how MOS does it (as I don't read Rust) so I'm making this up as I go along. I'm using the following file as a test case for the work:
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
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:
| 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
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:
... 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
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:
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 )
The second one to:
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 )
And the last one to:
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 )
This works, and it was easy to implement the VM. 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.
![Glasses. Titanium, not steel. [Self-portrait with my new glasses]](https://www.conman.org/people/spc/about/2025/0925.t.jpg)