Devlog 32 Fixing Bugs
January 04, 2023
Log 32
In this session I’ll focus on fixing some bugs I discovered with the data stack.
Fixing DSP
In devlog 2, I introduced the idea of a TOS
(top of stack) register. It’s not new to Forth, but it was new to me. The idea was to use a saved register (s3
) to keep the top element of the stack. It would slightly simplify certain operations but also complicate others.
One of the biggest issues I’ve run into is regarding the initialization routine, particularly this line:
mv s3, zero # initialize TOS register
This sets the TOS
register to a known value: 0
. So far so good, however let’s take a look at our PUSH
macro:
# push register to top of stack and move TOS to DSP
.macro PUSH reg
addi sp, sp, -CELL # move the DSP down by 1 cell
sw s3, 0(sp) # store the value in the TOS to the top of the DSP
mv s3, \reg # copy register into TOS
.endm
It first decrements the DSP
(sp
) pointer by 1 CELL before storing the TOS
value into it, and then copying whatever register was pushed.
Here’s an example:
PUSH a0
Seems simple, but it’s bad. The very first PUSH
will actually end up copying the value 0
to the top of the DSP
(pointed at by sp
, which will be address 0x20004FFC
), and it will store the value from a0
into TOS
(s3
). So now, our stack actually has 2 elements! Oops!! In fact, the very first PUSH
should only write to TOS
without moving the stack pointer… thus ignoring the 0
in the TOS
, but I think coding for that condition is a bit ridiculous because the TOS
could potentially have a value we want, ex: if 0=
was the first command, then TOS
would contain 1
…
So, at this point I just want to get rid of the TOS
(s3
) register and only use the DSP
. The entire time I’ve always had to think extra hard about the TOS
register and now that I’ve encountered this bug, I just want it to disappear.
Fixing macros
To start, I’ll edit the macros in src/02-macros.s
to only use the DSP
. Here are the changes to the POP
macro:
.macro POP reg
- mv \reg, s3 # copy TOS to register
- lw s3, 0(sp) # load DSP value to register
+ lw \reg, 0(sp) # load DSP value to register
addi sp, sp, CELL # move the DSP up by 1 cell
.endm
and the PUSH
macro:
.macro PUSH reg
addi sp, sp, -CELL # move the DSP down by 1 cell
- sw s3, 0(sp) # store the value in the TOS to the top of the DSP
- mv s3, \reg # copy register into TOS
+ sw \reg, 0(sp) # store the value in the register to the top of the DSP
.endm
The above macros were simplified thanks to the removal of the TOS
. Similarly, we’ll also adjust the PUSHVAR
macro (which I think I coded incorrectly anyways):
.macro PUSHVAR var
addi sp, sp, -CELL # move the DSP down by 1 cell
- sw s3, 0(sp) # store the value in the TOS to the top of the DSP
li t0, \var # load variable into temporary
- lw s3, 0(t0) # load variable address value into TOS
+ sw t0, 0(sp) # store the variable value to the top of the DSP
.endm
Here we’re not loading the value pointed at by the variable anymore. Instead we’re just storing the memory address of the variable to the top of the stack. I guess we can then use @
to fetch the actual contents of those memory addresses.
Fixing primitives
That conveniently leads us to our next changes in src/08-forth-primitives.s
, where we’ll start by modifying FETCH
:
defcode "@", 0x0102b5e5, FETCH, NULL
- lw s3, 0(s3) # load address value from TOS (addr) into TOS (x)
+ lw t0, 0(sp) # load the top of stack into temporary
+ lw t0, 0(t0) # load the value from the temporary (addr)
+ sw t0, 0(sp) # store the value back the top of stack (x)
NEXT
Yikes! We’ve got way more instructions for this, because now we need to load the value in the stack pointer, get the address it points to, then store that back into the stack pointer. Slightly more complicated than simply loading from TOS
into TOS
.
We can test that it works in the terminal with:
latest @<Enter>
Now if everything worked well, we should have the memory address of word_SEMI
stored as the first entry in the data stack. Let’s check with GDB
:
(gdb) i r sp
sp 0x20004ffc 0x20004ffc
(gdb) x/xw 0x20004ffc
0x20004ffc: 0x080004d8
(gdb) x/xw 0x080004d8
0x80004d8 <word_SEMI>: 0x080004cc
Great!
Next we’ll modify STORE
by loading the top two stack entries into temporaries, and then storing one into the other:
defcode "!", 0x0102b5c6, STORE, FETCH
lw t0, 0(sp) # load the DSP value (x) into temporary
- sw t0, 0(s3) # store temporary into address stored in TOS (addr)
- lw s3, CELL(sp) # load second value in DSP to TOS
+ lw t1, CELL(sp) # load the DSP value (addr) into temporary
+ sw t0, 0(t1) # store x into addr
addi sp, sp, 2*CELL # move DSP up by 2 cells
NEXT
The instruction count remains the same, but we’re not messing with TOS
anymore. Let’s test it out by trying to store the value of latest
to the writeable memory address HERE
, which is set to 0x20000000
(the start of the dictionary) on initialization. In the terminal we’ll type:
here @ latest @ !<Enter>
Still following along? We just put HERE
in the stack, and then put LATEST
in the stack. Then we called STORE
which technically should store 0x080004d8
into 0x20000000
. Let’s check with GDB
:
(gdb) x/xw 0x20000000
0x20000000: 0x080004d8
Sweet!
Actually, at this point I’m starting to feel amazing. So far everything is working as I hoped. Let’s continue with ZEQU
, which was a buggy non-sensical little 1-liner:
defcode "0=", 0x025970b2, ZEQU, RSPFETCH
- seqz s3, s3 # store 1 in TOS if TOS is equal to 0, otherwise store 0
+ lw t0, 0(sp) # load the DSP value (x) into temporary
+ snez t0, t0 # store 0 in temporary if it's equal to 0, otherwise store 1
+ addi t0, t0, -1 # store -1 in temporary if it's 0, otherwise store 0
+ sw t0, 0(sp) # store value back into the top of the stack
NEXT
First I realized the ZEQU
should actually store -1
not 1
. In this case I’m using the exact same approach as sectorforth but with RISC-V instructions to “set if not equal” and then to decrement the value by 1. Let’s test it by storing the STATE
(should be 0
) in the DSP
, and then calling 0=
in the terminal with:
state @ 0=<Enter>
In GDB
we should find -1
as the top stack value:
(gdb) i r sp
sp 0x20004ffc 0x20004ffc
(gdb) x/dw 0x20004ffc
0x20004ffc: -1
Now let’s call 0=
again in the terminal:
0=<Enter>
And let’s inspect it again in GDB
:
(gdb) i r sp
sp 0x20004ffc 0x20004ffc
(gdb) x/dw 0x20004ffc
0x20004ffc: 0
Awesome!
The last primitives to fix are ADD
and NAND
which should be fairly similar as they have the same stack effects. Let’s start with ADD
:
defcode "+", 0x0102b5d0, ADD, ZEQU
POP t0 # pop value into temporary
- add s3, s3, t0 # add values and store in TOS
+ lw t1, 0(sp) # load DSP value (x2) into temporary
+ add t0, t0, t1 # add the two values
+ sw t0, 0(sp) # store the value into the top of the stack
Again, without the TOS
it’s a bit more complex. Here we’re still popping the top value from the stack into t0
, but we’re also loading the next top value into t1
(note: the POP t0
moves the stack pointer, so we’re still loading from offset 0
). Afterwards we’re adding the two registers and then storing the result back into the top of the stack.
Now let’s look at the bitwise NAND
:
defcode "nand", 0x049b0c66, NAND, ADD
POP t0 # pop value into temporary
- and s3, s3, t0 # store bitwise AND of temporary and TOS into TOS
- not s3, s3 # store bitwise NOT of TOS into TOS
+ lw t1, 0(sp) # load DSP value (x2) into temporary
+ and t0, t0, t1 # perform bitwise AND of the two values
+ not t0, t0 # perform bitwise NOT of the value
+ sw t0, 0(sp) # store the value into the top of the stack
NEXT
It’s almost identical to ADD
except we’re performing bitwise AND and NOT instead of ADD.
That completes our changes to the forth primitives. Let’s test ADD
and NAND
. We’ll start by trying to add the value of LATEST
to the value of LATEST
. In the terminal we’ll type:
latest @ latest @ +<Enter>
Now in GDB
, let’s get the address of LATEST
(aka word_SEMI
), right now it gives us 0x80004e8
(because we added some new instructions previously). Multiplying it by 2 should give us 0x100009D0
stored at the top of the stack.
Let’s check:
(gdb) x/xw word_SEMI
0x80004e8 <word_SEMI>: 0x080004dc
(gdb) i r sp
sp 0x20004ffc 0x20004ffc
(gdb) x/xw 0x20004ffc
0x20004ffc: 0x100009d0
Perfect!
Next we’ll see if NAND
works as expected by storing STATE
in the top of the stack. In the terminal we’ll type:
state @ state @ nand<Enter>
And in GBD
, since we know that STATE
is set to 0
when we’re in execute mode, performing a NAND
of 0
and 0
should give us -1
(remember this is bitwise, which flips all the 0 bits to 1, giving us 0xFFFFFFFF
or -1
):
(gdb) i r sp
sp 0x20004ffc 0x20004ffc
(gdb) x/dw 0x20004ffc
0x20004ffc: -1
Yesss!!!
That’s all for our forth primitives. The one final change is to remove the initialization of s3
in src/06-initialization.s
:
- mv s3, zero # initialize TOS register
Closing thoughts
This was a rather long session of writing and testing and editing code, but we did it! Not only did I fix some bugs in some of the primitives, but I also greatly simplified the data stack by getting rid of the TOS
.
In the next session, I’ll get the compiler working so I can finally add words to the user dictionary starting at address 0x20000000
(on the longan nano).