Devlog 34 Compiling Words
In this session I will work on compiling words, but before that I want to add 2 new features to my Forth: printing strings and rebooting the mcu.
The previous approach to printing strings over the UART was to repeat the same 2 lines of code for every character. To print ` ok\n` we would write this:
ok: li a0, CHAR_SPACE call uart_put li a0, 'o' call uart_put li a0, 'k' call uart_put li a0, CHAR_NEWLINE call uart_put
This works fine for short strings, but it’s quite bothersome and ugly for longer strings.
Here’s my short implementation of a UART “print” function:
uart_print: mv s3, ra # save the return address uart_print_loop: beq a1, a2, uart_print_done # done if we've printed all characters lbu a0, 0(a1) # load 1 character from the message string call uart_put addi a1, a1, 1 # increment the address by 1 j uart_print_loop # loop to print the next message uart_print_done: mv ra, s3 # restore the return address ret
It accepts 2 arguments:
a1which contains the memory address of the start of a string (I’ll show an example later).
a2which contains the address of the string + its length.
uart_print starts by saving the
ra register and ends by restoring it. We do this because the
call uart_put would otherwise clobber
ra and it would be unable to return after printing.
uart_print_loop simply loops over each character in the string, printing a character at each iteration. It increments the string’s address (
a1) until it’s the same as
Here’s how we would use it instead of the above
ok: la a1, msg_ok # load string message addi a2, a1, 6 # load string length call uart_print # call uart print function
And we could define the
msg_ok string like this:
msg_ok: .ascii " ok\n"
.ascii string is NOT null terminated, and it must be aligned to 2 bytes. In other words a 3 or 5 byte string would not work.
Let’s do something similar to
error: la a1, msg_error # load string message addi a2, a1, 4 # load string length call uart_print # call uart print function
And define the
msg_error like this:
msg_error: .ascii " ?\n"
Rebooting the MCU
I often find myself wanting to test a clean slate of the Forth, without physically resetting the device (which requires restarting openocd and gdb). So I decided to add a new primitive called
reboot, which jumps directly to the
_start initialization procedure:
# reboot ( -- ) # Reboot the entire system and initialize memory defcode "reboot", 0x06266b70, REBOOT, NULL j reboot # jump to reboot
I then had to modify
FETCH to link to
REBOOT instead of
-defcode "@", 0x0102b5e5, FETCH, NULL +defcode "@", 0x0102b5e5, FETCH, REBOOT
Now let’s define the
reboot: la a1, msg_reboot # load string message addi a2, a1, 12 # load string length call uart_print # call uart print function j _start # reboot when print returns
It’s pretty much the same as
error, with a different string message and different jump to address. Here’s the message:
msg_reboot: .ascii " rebooting\n"
So now typing
reboot<Enter> in the terminal will display the string
rebooting and everything will be reset as if we first booted the device. Of course I realize this might be problematic once interrupts are enabled, but I think by then I’ll be able to remove this primitive and functionality.
Now the final missing element of this Forth, compiling words!!
The first change is to fix some minor issues in our macros. In 3 macros we’re decrementing the
sp stack pointer by 1 CELL before performing an operation, which is fine except when that operation involves the
sp pointer. Let’s change the
PUSH macro first, and I’ll explain the difference afterwards:
.macro PUSH reg + sw \reg, -CELL(sp) # store the value in the register to the top of the DSP addi sp, sp, -CELL # move the DSP down by 1 cell - sw \reg, 0(sp) # store the value in the register to the top of the DSP .endm
Here we moved the
sw instruction so it’s performed first, before decrementing the pointer. But we’re also storing it at the
-4 offset. This was necessary for something like
PUSH sp to work, where we want to push the current
sp address not the next address (
sp - 4).
We’ll make a similar change to
.macro PUSHRSP reg + sw \reg, -CELL(s2) # store value from register into RSP addi s2, s2, -CELL # decrement RSP by 1 cell - sw \reg, 0(s2) # store value from register into RSP .endm
And finally we’ll also modify
PUSHVAR to load the register and then store it in
sp - 4 before moving the
sp pointer down by 1 CELL.
.macro PUSHVAR var - addi sp, sp, -CELL # move the DSP down by 1 cell li t0, \var # load variable into temporary - sw t0, 0(sp) # store the variable value to the top of the DSP + sw t0, -CELL(sp) # store the variable value to the top of the DSP + addi sp, sp, -CELL # move the DSP down by 1 cell .endm
COLON primitive (inner interpreter), we need to do the exact same thing as in
process_token (outer interpreter) before and after calling
token, so let’s replace the existing code:
defcode ":", 0x0102b5df, COLON, LATEST - li a0, TIB # load TIB into W - li t3, TOIN # load the TOIN variable into unused temporary register - lw a1, 0(t3) # load TOIN address value into X working register + li t3, TOIN # load TOIN variable into unused temporary register + lw a0, 0(t3) # load TOIN address value into temporary call token # read the token + # move TOIN + add t0, a0, a1 # add the size of the token to TOIN + sw t0, 0(t3) # move TOIN to process the next word in the TIB + # bounds checks on token size - beqz a1, error # error if token size is 0 + beqz a1, ok # ok if token size is 0 li t0, 32 # load max token size (2^5 = 32) in temporary bgtu a1, t0, error # error if token size is greater than 32 - # store the word then hash it - sw a0, 0(t3) # store new address into TOIN variable call djb2_hash # hash the token
COLON’s first few lines are identical to
We’ll also need to fix a bug I discovered when storing the
code_EXIT address at the end of a word:
- sw t1, 0(t0) # store the codeword address into HERE + sw t1, 0(t2) # store the codeword address into HERE
HERE address was stored in
t2 but I accidentally used
t0 which means
EXIT would not be written to the correct memory location.
Now let’s look at our
compile function, called from the
process_token (outer interpreter). The first step is to find the codeword address, which is 2 CELLs down:
compile: addi t0, a1, 2*CELL # increment the address of the found word by 8 to get the codeword address
Then we’ll load
HERE into a temporary, and store the codeword in there:
li t1, HERE # load HERE variable into temporary lw t2, 0(t1) # load HERE value into temporary sw t0, 0(t2) # write the address of the codeword to the current definition
Afterwards we can increment
HERE by 1 CELL and store its value back, before jumping back to process the next token:
addi t0, t2, CELL # increment HERE by 4 sw t0, 0(t1) # store new HERE address compile_done: j process_token
At least.. I think that’s it. Let’s try to compile a word in the terminal:
: dup sp@ @ ;<Enter> ok
So far so good, maybe? Let’s check the user dictionary with
GDB. This should store 6 values in memory starting from
0x20000000, three values for
dup (link, hash, codeword), one address for
DSPFETCH), one address for
FETCH) and one address for
(gdb) x/6xw 0x20000000 0x20000000: 0x08000650 0x03886bce 0x080003f4 0x080005b0 0x20000010: 0x08000598 0x080005ec
Now let’s look at each value:
(gdb) x/xw 0x08000650 0x8000650 <word_SEMI>: 0x08000644
That’s our link to the previous word. Then
0x03886bce is the hash of the word
(gdb) x/xw 0x080003f4 0x80003f4 <.addr>: 0x080003e4 (gdb) x/xw 0x080003e4 0x80003e4 <docol>: 0xfe992e23
Next we have the address of
.addr which points to
docol. This is where I’m still a bit confused, and it might be totally wrong.
Next let’s examine the remaining 3 values:
(gdb) x/xw 0x080005b0 0x80005b0 <code_DSPFETCH>: 0x08000284 (gdb) x/xw 0x08000598 0x8000598 <code_FETCH>: 0x08000264 (gdb) x/xw 0x080005ec 0x80005ec <code_EXIT>: 0x080002d4
All that looks pretty good to me. Let’s store a value in the stack, and then use
dup to duplicate it on the stack (which is what
sp@ @ does):
Well… I guess that doesn’t work. The word was definitely compiled and stored in memory, but there’s clearly something wrong in there. I have a feeling this might be related to the compiled
.addr -> docol address, but I’m not sure.
In the next session I’ll manually step through the execution of my newly defined
dup word, and see if I can find the problem. Hopefully I’ll be able to fix this in the next session, and then I’ll have a fully functional Forth. Yay!