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

Local Variable Library V1.2

For TurboForth V1.2

Written by Mark Wills

Updated 22nd March 2017

For Rumiya

Table of Contents

1 - Introduction

2 - Declaring Local Variables in a Colon Definition

3 - Storing Data in your Local Variables

4 - Nesting and Scope

5 - Recursion

6 - Overhead
    6.1 - Execution Overhead
    6.2 - Memory Overhead

7 - Locals Dictionary

8 - Compile Time and Run-time Performance

9 - Calls to @LOCAL

10 - Writing to Local Variables Overhead

11 - Local Variables Source Code

12 - Test Code

13 - Example: DUMP using locals

1 - Introduction

With all the emphasis that Forth puts on the stack it could be argued that it goes against the grain somewhat to use local variables in Forth. However, the truth is that some Forth code can become an ugly mess, full of stack juggling words (known in the Forth world as stackrobatics) that juggle data on the stack to get it into the correct order for our program to use. When one thinks about it, those stackrobatics are a waste of processor cycles; we're burning processor time just juggling data on the stack, instead of actually getting our work done. That's not good.

A practice has evolved in Forth where stack items that are 'in the way' can be placed on the return stack temporarily (using >R) and removed later (using R>), which can sometimes be a neat solution. However, it has a couple of drawbacks:

A neater solution is to implement local variables properly, rather than using the return stack, and to implement them in such a way that they interact with the data stack in a natural, Forth-like way. This author believes the following solution meets the above criteria.

The code presented here caters for up to fourteen named local variables per colon-definition. The local variables can have any name, but they should not be named after pre-existing words; in other words, don't name your locals DUP or DROP or SWAP. This is because during compilation of a colon-definition, the standard Forth dictionary is searched first, then (if the word was not found) the locals dictionary is searched. So, if your local has the same name as another word in the dictionary it will not be found. In any case, naming a local after a pre-existing word is just plain confusing. Don't do it!

2 - Declaring Local Variables in a Colon Definition

The word LOCALS{ is used to begin the definition of a list of local variables. Definition of locals is terminated with } as in the following example:

: computeArea ( w h -- area)
  LOCALS{ height width } 
  SET height   SET width
  height width * ;

In this example, two local variables are declared: height and width. The order in which local variables are declared is not important, since, unlike other local variable implementations, the locals do not have to be initialiased from the data stack (though you can do that if you want to - see the next section). Note that local variables are referenced in your code by their names. Naming a local variable in a colon definition causes its value (not its address) to be pushed to the stack. Local variables (in this implementation) work very similiarly to VALUEs.

3 - Storing Data in your Local Variables

Data is stored into your local variables from the data stack, with the words SET and +SET. SET and +SET are analogous to TO and +TO which are used with VALUEs.


    : TEST ( n4 n3 n2 n1 -- ) 
      LOCALS{ A B C D }

      SET D  SET C  SET B  SET A
      A . B . C . D .
    1 2 3 4 TEST
    1 2 3 4 ok:0

As can be seen, the local variables are populated directly from the data passed in on the stack. Note that the data is removed from the stack, as the local variables are loaded, as one would expect.

Note that, as shown in the stack signature, n1 was on the top of the stack when TEST was invoked, this was loaded into the local variable D with the phrase SET D, n2 was loaded into C, n3 into B and n4 into A.

Once your data has been stored in local variables, it can be accessed in any random order, simply by name; no stack juggling or use of the return stack is required.

In the example above, all four local variables are loaded from the data on the stack passed into TEST. However, (unlike most Forth local variable implementations) they don't have to be. Here's an example:

    : area ( width length -- sqArea triArea )
      locals{ sqArea triArea }
      * set sqArea
      sqArea 2/ set triArea doSomethingElse ;

As can be seen above, local variables in TurboForth are true local variables that you can use in any way you want.

Loading Locals Automatically from the Stack

Quite often, you will load the parameters passed into a definition into local variables at the start of the definition. In previous versions of the local variables library, it was necessary to use SET to load them, like this:

    : example ( a b c d )
      locals{ a b c d }
set d set c set b set a ... ... ... ;

Note that the locals are set "in reverse order" because (obviously) d is on the top of the stack, so it needs to be loaded first.

However, in version 1.2 of the library, there is a word provided to load the local variables for you:

    : example ( n1 n2 n3 n4 )
      >locals{ a b c d }
... ... ... ;

The word >LOCALS{ (which is pronounced "to locals") declares the local variables with the names provided, and loads them from the stack at run-time. In this case, four locals are declared (a, b, c & d) so at run-time four cells will be popped off the stack into the locals for you, in the order you would expect; specifically top of stack would go to d, next would go to c, and so on.

If you want to automatically load some locals from the stack and have some general-purpose locals for use in your definition then you need to use a trick. Let's say you are computing the area of a square and a triangle from the same two dimensions. You want to pass in the width and the height from the stack, but you want the local variables areaSq and areaTri to be available as general purpose local variables (they are going to receive the results of the area computations).

Here's how you do it:

    : areaSqTri ( w h -- sq tri )
      0 0
>locals{ width height areaSq areaTri }
width height * set areaSq areaSq 2/ set areaTri areaSq . areaTri . ;

You can probably see what's going on here. >LOCALS{ is going to compile code to initialise four locals from the stack because we've declared four locals, even though the stack signature (which is just a comment and has no run-time effect at all) says the code takes two parameters from the stack. So, we simply get around this by placing two dummy zeros on the stack prior to the generation of the local variables so that areaTri is loaded with the blue 0 and areaSq is loaded with the red 0.

If you run areaSqTri using 10 9 as input, you'll correctly see 90 and 45 reported as the areas of a square and a triangle, respectively.

4 - Nesting and Scope

Words that contain locals can be nested normally, as one does in Forth; words call words which call words etc. A words' local variables retain their values when it comes back into scope. For example:

In the above example, data is loaded into the local variables of TOM, but TOM immediately goes 'out of scope' as DICK is called (which subsequently calls HARRY). Note that when TOM eventually comes back into scope (after the execution of HARRY), the data loaded into its local variables is retained.

5 - Recursion

Recursion in TurboForth is achieved with the word RECURSE, which causes a recursive call to the word in which it occurs. For example:

When using recursion, it should be noted that each instance of the word will inherit its own set of local variables. Of course, the stack is used to communicate data into and out of the instances. The classic recursive factorial example above has been modified to show how deep the recursion goes. The current value of a counter is copied into local variable A of each instance. The value of the counter is increased with each instance of the word FACT. As the recursion 'un-winds' the value in the local variable is displayed. The following output is revealed:

    8 7 6 5 4 3 2 1 1 <TOP instance:8
    8 7 6 5 4 3 2 1 <TOP instance:7
    8 7 6 5 4 3 2 <TOP instance:6
    8 7 6 5 4 6 <TOP instance:5
    8 7 6 5 24 <TOP instance:4
    8 7 6 5 120 <TOP instance:3
    8 7 720 <TOP instance:2
    8 5040 <TOP instance:1
    40320 <TOP instance:0
    40320 ok:0

6 - Overhead

Execution Overhead

Overhead on the system is very small indeed. Colon definitions that use local variables incur a call to (allotLocals) at the beginning and end of the definition in order to allocate and de-allocate space on the locals stack.

Memory Overhead

After the locals library is loaded, all colon definitions that use local variables shall incur an extra 8 bytes of overhead. These are the calls to (allotLocals) as noted above.

All references to local variables are compiled as a @local with an in-line parameter representing the offset into the locals stack. Storing the offset parameter in-line makes the code smaller as it does not require a reference to LIT to push the value onto the stack prior to the call to @local. References to SET and +SET are compiled in the same way, which represents the same offset into the locals stack and a call to either (SET) or (+SET) respectively.

In addition, an area of memory is required to host the 'locals stack'. By default, this is currently set to >FFDE which is at the end of the 24K memory segment in the 32K memory expansion module on the TI-99/4A. Each call to (allotLocals) adjusts the stack by n bytes, according to the in-line parameter. The stack grows from higher memory addresses to lower memory addresses.

7 - Locals Dictionary

This implementations uses no dictionary space at all for the names of the locals variables. During compilation, the names of the locals are hashed and stored in a table beginning at >FFE0. Each local variable name is stored as a 16-bit unsigned hash, and stored in the table in the order in which they appear in the LOCALS{ definition. Thus their ordinal location in the table has a direct relation to their run-time offsets within the locals stack.

Once the colon definition containing locals has been compiled, the hashed locals are no longer needed, since the references to the locals within the colon definition have already been compiled into calls to @local.

8 - Compile Time and Run-time Performance

The code has been carefully designed such that, as far as possible, all time-consuming work is performed at compile-time rather than run-time, in order to minimise the performance penalty to a running progam that uses locals. FIND has been re-vectored, such that during compilation:

8.1 Performance Enhancements Compared to Version 1.0

This version of the library contains some enhancements to improve performance over the previous version:

The increase in performance, as well as producing smaller code is even more justification to use locals in preference to stack juggling. Stack juggling is quite literally a waste of time: you are moving things around on the stack instead of actually getting your intended work done. Placing key values in locals will actually both increase peformance, and make your code smaller.

Since the normal Forth dictionary is searched first, there is no measurable increase in compile time for words that do not use locals. Words that do use locals are slightly slower to compile, since, in the event that the word being sought is not in the dictionary, it must be hashed (which takes a small amount of time) before it can be searched for in the locals dictionary/hash table, as described above.

9 - Calls to @LOCAL

When a reference to a local variable is compiled, a call to @local is compiled, as in the following example:

   : TEST LOCALS{ A B C } A B C . . . ;

The above code is compiled as follows:

   (allotLocals) 3  \ reserve space for 3 cells on locals stack
   @LOCAL 4         \ fetch A
   @LOCAL 2         \ fetch B
   @LOCAL 0         \ fetch C
   . . .            \ . . .
   (allotLocals) -3   \ de-allocate reserved space

As can be seen, the overhead to fetch a local is very small. 4 bytes (6 bytes in version 1.0 of this library). It is both smaller and faster than fetching a value from an array using an address and an offset (which is exactly what happens within @local).

10 - Writing to Local Variables Overhead

Similarly, the overhead to write to a local variable is very small:

	: TEST LOCALS{ A B C } SET A   SET B   SET C   A B C . . . ;

Compiles to the following:

   (allotLocals) 3  \ allot space on locals stack for 3 locals
   (SET) 4          \ set a with a value from the data stack
   (SET) 2          \ set b with a value from the data stack
   (SET) 0          \ set c with a value from the data stack
   @local 4         \ fetch a from locals stack and push to data stack
   @local 2         \ fetch b from locals stack and push to data stack
   @local 0         \ fetch c from locals stack and push to data stack
   dot dot dot      \ display the three values on the data stack
   (allotLocals) -3   \ de-allocate the space on the locals stack

Note that all parameters are in-line, after the words, rather than as literals (as in the previous version) preceding the call.

The definition of (SET) is simply (version 1.0):

	: (SET) ( value offset -- )
    \ at runtime, set a local variable to value 'value'
_LS + ! ;

And, in versions 1.1 and 1.2:

   asm: (SET) ( value -- )
     _LS @@ r0 mov, \ get locals pointer
     r3 *+ r0 a, \ add in-line offset
     *sp+ r0 ** mov, \ move value to local

Whilst the definition of (+SET) is simply (version 1.0):

	: (+SET) ( value offset -- ) 
     \ at runtime add a value to a local variable
_LS + +! ;

And, in versions 1.1 and 1.2:

   asm: (+SET) ( value -- )
     _LS @@ r0 mov, \ get locals pointer
     r3 *+ r0 a, \ add in-line offset
     *sp+ r0 ** a, \ add value to local

11 - Local Variables Source Code

The following code (less comments and formatting) is available on block 37 of the utilities disk (available for download). It occupies 808 bytes of CPU memory (version 1.0 is 882 bytes).

12 - Test Code

The following tests the local variable code.

TEST executes a loop 10 times, showing the values of the four local variables, A, B, C & D each time through the loop. However, halfway through the loop, it calls TEST2, which executes 5 times. Note that TEST and TEST2 have different values in their local variables, and, crucially, note that when control returns to TEST from TEST2, the local variables re-inherit their original values. Thus, TEST and TEST2 have different local variables, accessed in the same way (A B C & D), which go in and out of scope as one would expect - in other words, they behave the same (in terms of scope) as local variables in any other programming language.

Which yields the following output:

Inside TEST:
A=1 B=2 C=3 D=4 A=1 B=2 C=3 D=4 A=1 B=2 C=3 D=4 A=1 B=2 C=3 D=4 A=1 B=2 C=3 D=4 Inside TEST2: A=7 B=8 C=9 D=10 A=7 B=8 C=9 D=10 A=7 B=8 C=9 D=10 A=7 B=8 C=9 D=10 A=7 B=8 C=9 D=10 Back inside TEST: A=1 B=2 C=3 D=4 A=1 B=2 C=3 D=4 A=1 B=2 C=3 D=4 A=1 B=2 C=3 D=4 A=1 B=2 C=3 D=4 A=1 B=2 C=3 D=4

Thus showing that when TEST comes back into scope, its local variables have been retained.

13 - Example: DUMP using locals

The following is an example of how one might use locals in an implementation of DUMP. DUMP is used to display to the contents of memory on the screen. The address, the next 8 bytes of memory, and an ASCII representation of those 8 bytes are displayed. DUMP expects the address and number of bytes to display on the stack, as follows:

   DUMP ( address count -- )

For example:

   $6000 50 DUMP

[1] This restriction does not apply universally to all Forth systems, but it does apply to TurboForth, which follows the 'classical' implementation architecture, which uses the return stack for nesting of loops.

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