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


Local Variable Library V1.0

For TurboForth V1.2

Written by Mark Wills

Document uploaded 19th May 2012

Note: This version is superceded by version 1.1, which can be found here.


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 are not initialiased from the data stack. 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.

Example:

    : 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 the stack 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. Note also that a local variable retains its value after use. So the following is valid:

    : TEST ( n -- ) 
      LOCALS{ A } 
      SET A  
      A A A A . . . . ;
	  
    99 TEST 
    99 99 99 99 ok:0

4 - Nesting and Scope

Words 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. A call to (allotLocals) is compiled into the beginning of colon definitions, (after the call to DOCOL, but before any user executable code). Additionally, a call to (freeLocals) is compiled as the last word of a colon definition by semi-colon.

Memory Overhead

After the locals library is loaded, all colon definitions shall incur an extra 12 bytes of overhead. These are the calls to (allotLocals) and (freeLocals) which are compiled into all colon definitions as noted above.

All references to local variables are compiled as a literal (which represents an offset into the local variables stack) and a call to @local. References to SET and +SET are also compiled as a literal, 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) 'grows' the stack by n bytes, according to the literal preceding the call. Each call to (freeLocals) 'shrinks' the stack by n bytes in a similar fashion. 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:

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:

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

As can be seen, the overhead to fetch a local is very small. 6 bytes. It is no larger (or slower) than fetching a value from an array using an address and an offset (which is exactly what happens within @local).

The definition of @local is simply:

	: @local ( index -- n ) \ fetch a local from the local stack
_LS + @ ;

Where _LS is the address of the top of the locals stack.

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:

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

The definition of (SET) is simply:

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

Whilst the definition of (+SET) is simply:

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

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 882 bytes of CPU memory.

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 >>