Devlog 12 Completing Semi
December 3, 2022
Log 12
Thanks to Moving Forth, I finally understand how COLON works, and the purpose of docol and enter. Let’s fix this.
Fixing COLON
In the last session I couldn’t figure out if I needed docol or enter as the codeword. When I put the two functions side-by-side, I realized they are almost identical! My docol was using the PUSHRSP macro, so I expanded it below:
docol:
addi s2, s2, -CELL # decrement RSP by 1 cell
sw s1, 0(s2) # store value from register into RSP
addi s1, a0, CELL # skip code field in W by adding a CELL, store it in IP
NEXT
enter:
sw s1, 0(s2) # store memory address from IP into RSP
addi s2, s2, CELL # increment RSP by CELL size
addi s1, a0, CELL # increment IP by W + CELL size
NEXT
After some analysis and brief testing, I concluded that docol is in fact the correct implementation. The enter function is an exact copy from derzforth, and it’s wrong because it first writes to the return stack before moving the pointer. In my Forth implementation, it points to the last entry in the stack, not the next available entry, similar to sectorforth and jonesforth, and the stack grows downward not upwards. So we first need to decrement the return stack pointer RSP before storing the new value. I want to store the code in the Y (a2) working register, so let’s adjust COLON:
- la t2, enter # load the codeword address into temporary # FIXME: enter or docol?
+ la a2, docol # load the codeword address into Y working register
I also want to remove the hardcoded CELL sizes in COLON and make sure we store the codeword from Y:
# build the header in memory
- sw t4, 0(t3) # store the address of the previous word
- sw a0, 4(t3) # store the hash
- sw t2, 8(t3) # store the codeword address
+ sw t4, 0*CELL(t3) # store the address of the previous word
+ sw a0, 1*CELL(t3) # store the hash
+ sw a2, 2*CELL(t3) # store the codeword address
# update HERE variable
- addi t3, t3, 12 # move the HERE pointer to the end of the word
+ addi t3, t3, 3*CELL # move the HERE pointer to the end of the word
Our new docol implementation will look like this:
docol:
PUSHRSP s1 # push IP onto the return stack
addi s1, a2, CELL # skip code field in Y by adding a CELL, store it in IP
NEXT
Starting SEMI
With : out of the way, I can focus on ; next. I’ll start by adjusting the hash of SEMI. It’s currently set to 0x0102b5e0 but since it’s an immediate word which must be executed right away, even if the STATE is set to 1 (compile mode), I’ll need to add the F_IMMED flag to the MSB by setting it to 1 (bitwise OR with 0x80000000).
-defcode ";", 0x0102b5e0, SEMI, COLON
+defcode ";", 0x8102b5e0, SEMI, COLON
This could lead to some confusion down the road, so I’ll document the 32-bit hash value below:
32-bit hash
+-------+--------+------------------+
| FLAGS | LENGTH | HASH |
+-------+--------+------------------+
3-bits 5-bits 24-bits
That’s the actual layout of the 32-bit hash. The first 3 bits represent flags, from the MSB: IMMEDIATE, HIDDEN, USER-DEFINED. The next 5 bits represent the length of the token. In our case we set it to 5 bits, which means it can have a maximum 32 characters (2^5). The remaining 24 bits represent the actual hash of the token.
In the djb2_hash function we’re performing a bitwise AND with the mask 0x00ffffff which lets us clear the first 8 bits in the hash. Then we add the length (shifted left by 24 bits) using a bitwise OR.
For example, the word exit should technically hash to 0x7c967e3f, but we clear the first 8 bits and it becomes 0x00967e3f, then we add the shifted length (4) and it becomes: 0x04967e3f.
Continuing SEMI
Moving forward with SEMI, at this point we’re essentially ending the compilation of the word. We’ll need to clear the HIDDEN flag, store the codeword for exit in memory, then move the HERE pointer.
Clearing HIDDEN will first require loading the hash from memory. We use LATEST to find out where it’s stored:
li t0, LATEST # copy the memory address of LATEST into temporary
lw t0, 0(t0) # load the address value into temporary
lw t1, 4(t0) # load the hash into temporary
Then we’ll load a bitmask used to unset the hidden bit (it’s the bitwise NOT of the hidden flag 0x40000000):
li t2, 0xbfffffff # load hidden flag into temporary (~F_HIDDEN)
Then we can proceed to unhiding, or revealing the word and writing it back to memory:
and t1, t1, t2 # unhide the word
sw t1, 4(t0) # write the hash back to memory
Completing SEMI
The final steps in the semicolon primitive are to update the HERE variable, move the HERE pointer to the end of the word definition, and return the interpreter’s STATE to 0, which is execute mode instead of compile mode.
First we’ll load the address of HERE and update it with the address of the exit codeword:
# update HERE variable
li t0, HERE # copy the memory address of HERE into temporary
la t1, code_EXIT # load the codeword address into temporary # FIXME: why not body_EXIT?
sw t1, 0(t0) # store the codeword address into HERE
Notice I’ve got another question mark regarding loading the exit codeword. Looking at derzforth shows that it should jump at code_EXIT but I’m wondering if it shouldn’t be body_EXIT or word_EXIT. I’ll need to read more about this first.
For now I’ll just continue and move the HERE pointer:
# move HERE pointer
addi t1, t1, CELL # move the HERE pointer by 1 CELL
sw t1, 0(t0) # store the new address of HERE into the HERE variable
And finally, we update the STATE variable:
# update the STATE variable
li t0, STATE # load the address of the STATE variable into temporary
sw zero, 0(t0) # store the current state (0 = execute) back into the STATE variable
That’s almost identical to what we did in COLON except we set it to 0 instead of 1.
Setup registers
Now I want to initialize some registers in the _start function so I can get to testing the .elf and .bin files.
First we initialize the stack pointers:
la sp, __stacktop # initialize DSP register
la s1, interpreter # initialize IP register
li s2, RSP_TOP # initialize RSP register
mv s3, zero # initialize TOS register
I set the IP register (s1) to point to the interpreter, but that might need to change.
Then we ensure the function parameter registers are initialized to zero:
mv a0, zero # initialize W register
mv a1, zero # initialize X register
mv a2, zero # initialize Y register
mv a3, zero # initialize Z register
Next we’ll store some values in the variables:
li t0, STATE # load STATE variable
sw zero, 0(t0) # initialize STATE variable (0 = execute)
A nifty shortcut here, since zero is a register (x0), we can write it directly to a memory address without first needing to load it to a temporary like li t0, 0.
We’ll need to set TOIN to the same address as TIB, basically the terminal input buffer’s current “in” location will be the start of the buffer:
li t0, TIB # load TIB memory address
li t1, TOIN # load TOIN variable
sw t0, 0(t1) # initialize TOIN variable to contain TIB start address
Next we’ll need to set HERE to be the same address as the start of the RAM because there’s nothing stored there yet:
li t0, RAM_BASE # load RAM_BASE memory address
li t1, HERE # load HERE variable
sw t0, 0(t1) # initialize HERE variable to contain RAM_BASE memory address
Finally, we’ll make sure LATEST points to the latest dictionary word we defined (SEMI):
la t0, word_SEMI # load address of the last word in Flash memory (;) for now
li t1, LATEST # load LATEST variable
sw t0, 0(t1) # initialize LATEST variable to contain word_SEMI memory address
That completes my Forth initialization routine, but I’m not even sure if it’s correct. I am however certain it will change in the future because some of those values will also need to be initialized when there’s an error.
Closing thoughts
I believe the next step after initialization is for the code to jump to the interpreter, but that has yet to be written! (and I’m not even sure!) In the next session, I’ll focus on testing what I’ve written so far, directly on the Longan Nano Lite. Then I’ll jump to the 2 missing primitives: key and emit - for IO.