We have a working system with unit tests in place, it’s time to start moving more of the system to Forth itself and eventually to assembly language and even a tiny bit of direct machine code. Think of what we have now as scaffolding. We can do an incremental migration; all the while with a working, tested system at each step.
Already a good portion of our words are defined in Forth itself with a only small set of primitives defined in F#. In this post we’ll migrate all the stack-related words. We did some nifty trimming a few posts back when we redefined DUP, SWAP, OVER and ROT in terms of PICK and ROLL. Now we’re going to go even further and define even PICK and ROLL in terms of simpler primitives. We’ll also redo DROP, DEPTH and CLEAR as well as the return stack words (>R, R> and R@), the loop counts (I, J and K) and even the print-stack word (.S).
The basic strategy will be to move the stacks to memory and then to define everything in terms of low-level memory manipulation.
So far, memory has been used exclusively by the dictionary. Now we’re going to need to throw into the mix a pair of stacks and I’d like to reserve a portion for “system variables.” A common layout is to grow the dictionary up from low memory and the stacks down from high memory.
This partitions our little simulated memory store nicely. Similar to the way we’ve handled the dictionary, each stack will be maintained by a simple index register; indicating the top of the stack (which really grows down in the diagram). Popping the stack will simply mean retrieving the value to which the index points and then incrementing the index. Pushing will mean decrementing the index and storing there.
There will be no safely checks for stacks or the dictionary overrunning their partitions. Danger is all part of the fun! (we'll add back stack underflow checks later)
We’re going to use the “System” area for a set of memory mapped registers among other things. These registers will represent the stack and the dictionary pointers with designated cells representing each. This will make them easy to manipulate with ! and @ without having to add new primitive words.
let s = 0 // data stack
let r = 1 // return stack
let h = 2 // dictionary pointer
Initialize the registers per our memory layout:
mem.[h] <- 0x0400
mem.[s] <- 0x2FFF
mem.[r] <- 0x3FFF
Appending to the dictionary will be based on this register value instead of the mutable we were using before:
let append v = mem.[mem.[h]] <- v; mem.[h] <- mem.[h] + 1
We’ll add Forth psudo-constants too for these memory mapped register locations:
: S 0 ;
: R 1 ;
: H 2 ;
And we can go ahead and get rid of our F# definition (define "HERE" (fun () -> push h)) and instead go with:
: HERE H @ ;
Our current implementation of the stacks is based on F# Lists and pattern matching:
let mutable stack = 
let underflow () = failwith "Stack empty"
let push value = stack <- value :: stack
let pop () = match stack with h :: t -> stack <- t; h | _ -> underflow ()
let mutable rstack = 
let rpush value = rstack <- value :: rstack
let rpop () = match rstack with h :: t -> rstack <- t; h | _ -> underflow ()
As nice as this is, we’re going to ditch it in favor of a lower-level approach that will be more accessible from within Forth. We could implement the stacks entirely in Forth, but we still have some F# code playing around with them, so for now we’ll do the following.
We define functions for pushing and popping relative to a particular given register:
let push' reg value = mem.[reg] <- mem.[reg] - 1; mem.[mem.[reg]] <- value
let pop' reg () = mem.[reg] <- mem.[reg] + 1; mem.[mem.[reg] - 1]
Then redefine our existing functions in terms of these:
let push = push' s
let pop = pop' s
let rpush = push' r
let rpop = pop' r
We get to tear away a bunch of F# and replace it with Forth. First a couple of standard words to address both ends of the stack:
: S0 12287 ; \ bottom of the stack
: SP@ S @ ; \ current top of stack
Starting with the easy ones, our F# definitions of DEPTH, CLEAR and DROP are:
define "DEPTH" (fun () -> stack.Length |> push)
define "CLEAR" (fun () -> stack <- )
define "DROP" (fun () -> stack <- match stack with _ :: t -> t |_->underflow())
Easy enough. Replace with:
: DEPTH ( -- n) S0 SP@ - 1- ;
: CLEAR ( --) S0 S ! ;
: DROP ( a -- ) SP@ 1+ S ! ;
I’m not going to go into great detail about each of these. Hopefully the original F# code makes the intent clear and parsing the (I assume unfamiliar) Forth code is part of the fun.
Next, let’s switch to the return stack. We had the basic “push”, “pop” and “peek” equivalents and had several “counter” words used to access loop indices being stored on the return stack:
define ">R" (fun () -> pop () |> rpush)
define "R>" (fun () -> rpop () |> push)
define "R@" (fun () -> push rstack.Head)
let counter name offset = define name (fun ()-> List.nth rstack offset |> push)
counter "I" 1
counter "J" 3
counter "K" 5
A bit tedious, but not too bad to redo in Forth:
: >R R @ DUP DUP 1- R ! @ R @ ! ! ;
: R> R @ 1+ @ R @ @ R @ 1+ ! R @ 1+ R ! ;
: R@ R @ 1+ @ ;
: COUNTER 2* 3 + R @ + @ ;
: I 0 COUNTER ;
: J 1 COUNTER ;
: K 2 COUNTER ;
Part of the difficulty here was to be very careful of the state of the return stack. We have to remember that it’s being actively used while walking these definitions. For example, we could factor all the “R @ 1+” to a separate word (say FOO), but then a new return address would have been pushed and messes things up before calling FOO.
Next, PICK and ROLL are very interesting. Remember that they were the basis of the other stack manipulation words (DUP, SWAP, OVER and ROT):
define "PICK" (fun () -> pop () |> List.nth stack |> push)
define "ROLL" (fun () ->
let i = ref (pop ())
match List.partition (fun _ -> i := !i - 1; !i >= 0) stack with
| (h, v :: t) -> stack <- v :: h @ t | _ -> underflow ())
By moving these last two to Forth, we have no more F# definitions at all for doing stack manipulation! PICK is easy, but ROLL takes quite some work; having to remember the nth value (using the return stack for temporary storage), walk along shifting all the values, and finally restore the nth value to the top:
: PICK SP@ + 1+ @ ;
: ROLL SP@ 1+ + DUP @ >R BEGIN DUP >R 1- DUP @ R> ! DUP SP@ 2+ = UNTIL DROP R> SP@ 1+ ! ;
I doubt this definition of ROLL is ideal, but it will do until we can redefine it once more in assembly. The biggest difficulty with ROLL is that we can’t use things like SWAP in the definition without a circular dependency and so have to do some tricky shuffling. I’ll step through an example of how it works in a moment.
Finally, we may as well redefine printing of the stack while we can. This:
define ".S" (fun () -> stack |> List.rev |> List.iter print)
Becomes the following, and we’re done!
: .S SP@ 1- S0 1- 2DUP < IF DO I @ . -1 +LOOP THEN ;
I have no idea how most Forth devs visualize things as they work, but below is what I do.
In general it’s a bad idea to build up lots of stuff on the stack. In fact, “stack juggling” is a telltale sign that you’re doing it wrong. Sometimes though it’s unavoidable (as in our definition of ROLL with the circular dependency constraint). I must admit I broke a sweat coming up with the above definition.
Let’s play “human computer” and step through an execution of the phrase “1 2 3 2 ROLL” – lifting out the 1, resulting in 2 3 1 on the stack. Splaying out the process into a table with stack states and fragments of executed code. Addresses are shown as circled values to which they point.
Data Stack >
< Return Stack
1 2 3 2
1 2 3 ❶
SP@ 1+ +
Addr of Nth value
1 2 3 ❶ 1
Get Nth value
1 2 3 ❶
Move to temp
Loop: shift values
1 2 3 ❷
1 2 3 ❷ 2
1 2 3 ❷ 2 ❶
2 2 3 ❷
2 2 3 ❷ 0
DUP SP@ 2+ =
2 2 3 ❷
Continue until TOS
Repeat step 5
2 2 3 ❸
Repeat step 6
2 2 3 ❸ 3
Repeat step 7
2 2 3 ❸ 3 ❷
Repeat step 8
2 3 3 ❸
Repeat step 9
2 3 3 ❸ -1
Repeat step 10
Reached TOS, done
2 3 3
2 3 3 1
2 3 3 1 ❸
2 3 1
Notice at the very end there (step 19), we had the address we wanted sitting on the stack but had to throw it away and recalculate after slipping in the temp value because we’re not able to use SWAP.
No new tests this time, but all of the 120 or so added earlier still pass.