<< Home | About Forth | About TurboForth | Download | Language Reference | Resources | Tutorials | YouTube >>


Local Variables for the Common Man

For Isla

I wrote a local variables library for TurboForth some years back. It was/is quite sophisticated; Forth words could have their own private, named variables. Nice.

Just recently I find I'm interested in writing less code, not more. I find I'm more interested in what code I can do *without* rather than the code I need. This leads to very interesting thought-exercises. It is very interesting to strip code away and arrive at the simplest code you can come up with that still gets the job done.

With that in mind, I recently took another look at local variables. For some folk, local variables in Forth are an anathema. I disagree. They reduce stack "juggling", or "stack traffic" where you are just juggling items on the stack to get them into the order you need them in. *Not* spending CPU cycles on juggling is getting more useful work done. Having named local variables in Forth (i.e. local variables that you can give any name to) is very nice, but we can really live without them. In assembly language on the TMS9900, we have 16 global "variables" - the registers. They are named R0 to R15. If we BLWP into a subroutine in a new workspace, we have 16 local variables, also called R0 to R15. The names are fixed, and we seem to get along with them just fine. So why not just do the same with local variables?

With that in mind, I thought I would write something to be as economical as possible. I was very pleased with all the code that I *hadn't* written, so I thought I would share what I haven't written here. :-)

The first problem to solve is where to put the local variables. We can't put them on the Forth data stack, because they would get in the way of other data that words are pushing/pulling to/from the stack. Imagine this word:

: A ( -- ) B C D E ;

Imagine that all of the words B C D and E use local variables. Furthermore, these words may internally call other words that also use local variables - maybe B calls Y and Y uses locals, and Y calls Z and Z uses locals.

We need a locals stack.

Well, a stack is just an area of memory with a pointer that points to the top of the stack:

$ff00 value lsp \ locals stack pointer

So, here's a VALUE (a type of variable) called 'lsp' (locals stack pointer). We're going to place our locals stack at >FF00 at the end of RAM.

Now we need some words that can store values on the data stack. Let's implement three local variables, A, B and C. What are we going to call these words? Well, for storing data in the variables (that is, taking something off the stack and storing it in a local variable) how about >a >b and >c? The arrow before the variable name shows something "going into" the variable. It's a picto-gram. Similarly, for reading from a local variable, (reading from the variable and pushing onto the stack) how about a> b> and c>? The arrow shows something leaving the variable.

Looks pretty good to me.

So, each word that uses local variables can have three local variables, a,b, and c. That's 6 bytes.

    +-------+
--> |   A   | 2 bytes
    +-------+
    |   B   | 2 bytes
    +-------+
    |   C   | 2 bytes
    +-------+

As can be seen, the locals stack pointer (lsp) is pointing to the top of the locals stack. So local variable A will be stored at the address in lsp, local variable B at lsp+2, and local variable C at lsp+4. Simple.

Here's the code for writing to the local variables. Note the stack signatures.

: >a ( n -- ) lsp ! ;
: >b ( n -- ) lsp 2+ ! ;
: >c ( n -- ) lsp 4 + ! ;

And here's the code for reading from the local variables. Again, note stack signatures.

: a> ( -- n ) lsp @ ;
: b> ( -- n ) lsp 2+ @ ;
: c> ( -- n ) lsp 4 + @ ;

Now we need a word to make some space on the locals stack. Again, I'll use a pictogram:

: lsp-> ( -- )  6 +to lsp ;

Here, the -> is pointing "upwards" on an imaginary number line, indicating that the word increases the lsp.

And here's a word to decrease the local stack pointer:

: <-lsp ( -- ) -6 +to lsp ;

We're nearly done. All we need to do now is have some method of using the local variables in a Forth word. After some experimenting, the simplest approach I could come up with was to have a new word for : (which is used to create new words) that indicates that we want to create a new word, but with the special property of having access to local variables.

For this, I chose :: (two colons).

So instead of:

: fred ( -- ) some clever code here ;

We have:

:: fred ( -- ) some clever code here ;

Both are identical, but the word created with :: has access to local variables.

Here's the code:

: :: : compile lsp-> ;

That actually looks quite confusing, so let's break it down:

Finally, we need to terminate the definition, just like ; in a "regular" word.

: ;; compile <-lsp [compile] ; ; immediate

Again, we use : to create a new word called ;; and this word will compile a reference to <-lsp into the word under creation, thus re-claiming the locals stack space that the word will use at runtime. We then want the word to run the normal ; action to complete the word compilation. Well, ; is an immediate word so we use [compile] to override this behaviour. Then we terminate the ;; itself with ; and we make it immediate, so that it matches the behaviour of ;

And voila. We're done. Look how much code it isn't:

$ff00 value lsp \ locals stack pointer
: >a ( n -- ) lsp ! ;
: >b ( n -- ) lsp 2+ ! ;
: >c ( n -- ) lsp 4 + ! ;
: a> ( -- n ) lsp @ ;
: b> ( -- n ) lsp 2+ @ ;
: c> ( -- n ) lsp 4 + @ ;
: lsp-> ( -- )  6 +to lsp ;
: <-lsp ( -- ) -6 +to lsp ;
: :: : compile lsp-> ;
: ;; compile <-lsp [compile] ; ; immediate

We've just added the ability for Forth words to have true, stacked access to local variables, and it took us 188 bytes!

Let's test it and see if it works:

:: test2 ( n1 n2 n3 -- )
    3 * >c
    3 * >b
    3 * >a
    ." Test 2:" a> . b> . c> . cr
;;

:: test1 ( n1 n2 n3 -- )
    cr
    2* >c
    2* >b
    2* >a
    a> b> c> test2
    ." Test 1:" a> . b> . c> . cr
;;
1 2 3 test1

If you run this, you get the following output:

Test 2: 6 12 18
Test 1: 2 4 6

Explanation of the code:

Hence we have proved that we can nest calls to words to contain their own local variables and they work as expected and don't interfere with each other.

And all in 188 bytes.

Enjoy your Forth!

11 January 2018


<< Home | About Forth | About TurboForth | Download | Language Reference | Resources | Tutorials | YouTube >>