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.