Jump to content

Photo

Forth: Local Variables for the Common Man

forth turboforth local variables

11 replies to this topic

#1 Willsy ONLINE  

Willsy

    River Patroller

  • 3,086 posts
  • Location:Uzbekistan (no, really!)

Posted Wed Dec 13, 2017 12:59 PM

Local Variables for the Common Man
 
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.
 
        +-------+
lsp --> |   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+6. 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 by :: has access to local variables.
 
Here's the code:
 
: :: : compile lsp-> ;
 
That actually looks quite confusing, so let's break it down:
 
  • The first colon means "hey, here comes a new word".
  • The double colon is the name of the new word, so we have "hey, here comes a new word called ::"
  • The third colon just includes the behaviour of : *in* the new word! So we have "hey, here comes a new word called :: and I want it do the same thing as : does, thanks."
  • The "compile lsp->" part will ensure that when :: runs (when a new word with locals is being created) a reference to our lsp-> word will be compiled into *that* word (the word that is bing created). Hence, the locals stack will move down memory, and the word will get its own space for its locals at run-time.
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:
  • We put 1 2 3 on the stack. Then we call test1.
  • Test1 multiples the top of the stack (n3 in the stack signature) by 2, and stores it in local variable c. In doing so, it is removed from the stack.
  • The next item on the stack (2) is multiplied by 2, and stored in b.
  • Then the last item on the stack (1) is multipled by 2, and stored in a.
  • We then get those stored values out of the local variables and back on to the stack, and call test2.
  • Test2 does a similar thing: Takes the values off the stack, multiolying them by 3 and storing them in *its* local variables. Then it displays them.
  • Then, control is returned to test1 just after the call to test2.
  • Now, the local variables that were in test2 have gone, and the local variables that are in test1 are "back in scope" and we prove that by displaying them. 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!

Edited by Willsy, Thu Dec 14, 2017 4:14 AM.


#2 Lee Stewart OFFLINE  

Lee Stewart

    River Patroller

  • 3,832 posts
  • Location:Silver Run, Maryland

Posted Wed Dec 13, 2017 6:45 PM

This is very nice.

 

I would have to do something a little differently in fbForth 2.0 because the parameter stack starts in high RAM just below the TIB at >FFA0.  I suppose I could guess how much room the parameter stack might need and put it that far below.  It could also be set up in the dictionary with a limit as I did when you and I ported your string library to fbForth 2.0.  Oh, wait—another idea just occurred to me.  I must chew on it a bit before I make a fool of myself.  Later...

 

...lee



#3 Willsy ONLINE  

Willsy

    River Patroller

  • Topic Starter
  • 3,086 posts
  • Location:Uzbekistan (no, really!)

Posted Thu Dec 14, 2017 2:08 AM

He he! Glad you're inspired! You could just CREATE a buffer to hold your locals in and have lsp point at it, so the locals live in "application/dictionary space". If you have 60 bytes you've got space for locals nested ten definitions deep. That's surely plenty of space. 

create locals 30 cells allot \ locals stack
locals value lsp 
: >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

Boom!  :D


Edited by Willsy, Thu Dec 14, 2017 8:49 AM.


#4 TheBF OFFLINE  

TheBF

    Dragonstomper

  • 821 posts
  • Location:The Great White North

Posted Thu Dec 14, 2017 8:47 AM

Nice work.  I will create a set of these words and play with them. I have TIB up at the top of memory, but I have a crude HEAP in low memory where I can implement this.

 

I have been pondering locals for the TMS9900 myself. 

I keep rolling around the idea of using free registers and a set of words to operate directly on the register variables.

( I am just a little jealous that GCC can do this) :-)

Since memory and registers are the same thing in 9900 land I think your method could accommodate that too with a little tweaking.

Need to study it more.

 

Another thing that goes with locals in memory is using the mem-to-mem architecture ability of the 9900 and creating words that operate variable to variable rather than going through the stack.  I think there are some nice opportunities there.

 

Thanks for this Willsy.



#5 Willsy ONLINE  

Willsy

    River Patroller

  • Topic Starter
  • 3,086 posts
  • Location:Uzbekistan (no, really!)

Posted Thu Dec 14, 2017 9:03 AM

Nice work.  I will create a set of these words and play with them. I have TIB up at the top of memory, but I have a crude HEAP in low memory where I can implement this.

 

Thanks for this Willsy.

 

No worries. Glad you like it!

 

While you're in a pondering mood, you might want to have a look at the TurboForth SAMS Library which allows full use of the SAMS memory expansion with no extra effort required by the Forth programmer whatsoever. That would be pretty cool in Camel Forth 99.



#6 ramidavis OFFLINE  

ramidavis

    Chopper Commander

  • 119 posts

Posted Thu Dec 14, 2017 9:03 AM

...


Edited by ramidavis, Tue Jan 2, 2018 10:47 AM.


#7 Willsy ONLINE  

Willsy

    River Patroller

  • Topic Starter
  • 3,086 posts
  • Location:Uzbekistan (no, really!)

Posted Thu Dec 14, 2017 9:06 AM

Your making me want learn forth, and that's a good thing :-)

 

Excellent! That's why us Forth nerds are posting. If you're curious about learning Forth, start here. It's written for my Forth system, TurboForth, but I think most of the stuff presented would work with any of the three Forths available for the 4A, so choose your weapon and enjoy the Forth!



#8 TheBF OFFLINE  

TheBF

    Dragonstomper

  • 821 posts
  • Location:The Great White North

Posted Fri Dec 15, 2017 12:03 AM

 

No worries. Glad you like it!

 

While you're in a pondering mood, you might want to have a look at the TurboForth SAMS Library which allows full use of the SAMS memory expansion with no extra effort required by the Forth programmer whatsoever. That would be pretty cool in Camel Forth 99.

 

That's pretty cool too.  I will see what it takes.  I have to get my cross-compiler making programs larger than 8K first :-)



#9 TheBF OFFLINE  

TheBF

    Dragonstomper

  • 821 posts
  • Location:The Great White North

Posted Fri Dec 15, 2017 12:09 AM

Your making me want to learn forth, and that's a good thing :-)

I like dabbling in Basic dialects, and i even have a couple of old C projects that i have not touched in years, a dice game similar to yahtzee, and a strange little "tool" for picking random dominoes in a similar notation as used with dice.

I.E. 2d4+2 is dice notation for 2 four sided dice rolled and add 2 to the result, while for dominoes you could ask for s2m15x4 meaning "tiles have 2 sides {s2} max value is 15 {m15} and i want 4 please {x4}". Also works on three sided Tri-ominos sets, that is why the sides parameter is there. s3m5x1 would give you 1 Tri-omino. Program also supports chinese-domino sets with its duplicates via adding a "c" modifier like s2m6cx7 meaning "tiles have 2 sides {s2} max value is 6 {m6}, but use a chinese set {c}, and i want 7 please {x7}". Was planning on adding support for Mahjong tile sets, but never implemented it.

 

My C projects: (both command line, both have linux and windows versions)

[bo|ne] [ya|rd] (logo is suppose to look like 2 dominoes in case you are wondering. Named after the pile of unused tiles in the game, called the boneyard by some.)

https://sourceforge....ects/bone-yard/

attachicon.gifScreenshot.png

yaht-C (yaht / yahtzee like game, with multiple ways to display the dice.)

https://sourceforge....projects/yahtc/

attachicon.gif2.png

 

Forth will hurt your head for while and then the penny drops. It's so stripped down from normal languages it is easier to assume it's a kind of assembly language with some handy support  macros when your first give it a spin.

 

But if you take some time with it, you actually end up being a better programmer in all the other languages you use, just because you see more "behind the curtain" stuff in your mind. 

In that sense it is also like assembly language too.

 

You have some good support here with the three amigos assembled on Atariage.   

 

B



#10 Lee Stewart OFFLINE  

Lee Stewart

    River Patroller

  • 3,832 posts
  • Location:Silver Run, Maryland

Posted Sat Dec 16, 2017 11:12 AM

The idea that occurred to me back in post #2 was to reserve space in each word needing local variables the number of cells needed by that word.  Then I would define each >A , A> , etc. in terms of where the data area in each word begins, without the need of a local variable stack.  I have worked out how to reserve the space, but have yet to figure out how to reference the beginning of that space in the current word for use by the to/from words.  If I can work that out, it would provide a way for each word to have as many variables as required by that word without the need to anticipate the maximum number by all words.

 

...lee



#11 Willsy ONLINE  

Willsy

    River Patroller

  • Topic Starter
  • 3,086 posts
  • Location:Uzbekistan (no, really!)

Posted Sat Dec 16, 2017 12:07 PM

The idea that occurred to me back in post #2 was to reserve space in each word needing local variables the number of cells needed by that word.  Then I would define each >A , A> , etc. in terms of where the data area in each word begins, without the need of a local variable stack.  I have worked out how to reserve the space, but have yet to figure out how to reference the beginning of that space in the current word for use by the to/from words.  If I can work that out, it would provide a way for each word to have as many variables as required by that word without the need to anticipate the maximum number by all words.

 

...lee

 

In terms of referencing the beginning of the reserved space, that's something you could have ;; do. Something like this (off the top of my head - un-tested):

variable lap \ local address pointer 
: >a ( n -- ) lap ! ;
: a> ( -- n ) lap @ ;
: >b ( n -- ) lap 2+ ! ;
: b> ( -- n ) lap 2+ @ ;
: >c ( n -- ) lap 4 + ! ;
: c> ( -- n ) lap 4 + @ ;
 
: slp ( addr -- ) \ set local address pointer
  lap ! ;
 
:: : compile lit here 0 , \ place holder for *address* of private space
  compile slp \ at run time, set address of locals space
;
 
;; [compile] ;  ( end the definition)
  here swap ! ( back-fill the address place holder )
  3 cells allot ( reserve space for 3 locals )
; immediate

 

The above method reserves 3 cells just after the end of a definition. When a word defined with :: is executed, the very first thing it does is set lap (locals address pointer) with the address that was determined by ;; when the compiliation of the word completed. However, you cannot accomplish nesting with this method. Nesting is really only an issue if words recurse and use locals.

 

Forth SO freaking rocks!...



#12 Lee Stewart OFFLINE  

Lee Stewart

    River Patroller

  • 3,832 posts
  • Location:Silver Run, Maryland

Posted Sat Dec 16, 2017 12:48 PM

Using a method of storing the local variables with their word, at least, would require only a small address stack that operates like the return stack.  It could be placed a few cells above the sound stack in fbForth 2.0 without taking too much out of the return stack space.  I suppose we could also force a stack size limit, though that would slow it down a bit.

 

I still think it should be possible to do it without a stack, even though a stack is probably cleaner.

 

...lee







Also tagged with one or more of these keywords: forth, turboforth, local, variables

0 user(s) are browsing this forum

0 members, 0 guests, 0 anonymous users