Jump to content
IGNORED

Forth: The Cart Before the Horse (#5)


Willsy

Recommended Posts

Forth: The Cart Before the Horse (#5)
This is one is going to be brief.
If you've read any of the Forth primers, or any of the Forth proclamations that I and others make here on Atariage from time-to-time, you'll no doubt have read about how versatile and configurable the language is.
We're going to have a very brief look at that today. But it won't be the War And Peace tome that I wrote yesterday.
Brian Fox recently wrote some fascinating code for Camel99 that gives the language a more Basic-like syntax. As you probably know, in Forth, arguments and parameters to words (functions) come *before* the word/function that you want to call. This is because words/functions take their data, and put their data on the stack, so the stack has to be loaded before the word is called:
TI BASIC: CALL HCHAR(ROW,COLUMN,CHAR,REPEATS)
Forth: ROW COLUMN CHAR REPEATS HCHAR
This tends to put people off when they look at Forth. It just looks like gobbledy-gook; at least until you understand that ROW and COLUMN etc are going on the stack, and HCHAR removes them.
However, Forth is supposedly "the most flexible language of them all", "ultra malleable, "if you don't like it you can change it" blah blah blah. So, why don't we put our money where our mouth is and prove it?
Well alrighty then!
Inspired by Brian's look at HCHAR and VCHAR I thought it might be fun to demonstrate how the language can be changed to suit your preferences. There are some restrictions, sure, but I think you'll be impressed.
We're going to change Forth's HCHAR and VCHAR into TI BASIC's HCHAR and VCHAR, where the arguments come after the word.
So, again, let's recap:
TI BASIC: CALL HCHAR(ROW,COLUMN,CHAR,REPEATS)
Forth: ROW COLUMN CHAR REPEATS HCHAR
We're going to end up with: CALL HCHAR( ROW COLUMN CHAR REPEATS )
Furthermore, we want it to be sophisticated enough such that the parameters can be numbers (called literals in Forth parlance) or calls to other words, or complex Forth expressions that compute a value etc.
What might shock you is how much code is NOT required to do this.
I give you:
: CALL ( -- ) ; IMMEDIATE \ do absolutely nothing
 
: HCHAR( 
  ASCII ) WORD EVALUATE
  STATE @ IF COMPILE HCHAR ELSE HCHAR THEN ; IMMEDIATE
  
: VCHAR( 
  ASCII ) WORD EVALUATE
  STATE @ IF COMPILE VCHAR ELSE VCHAR THEN ; IMMEDIATE
Now, you can type this directly on the command line:
PAGE
CALL HCHAR( 10 10 42 10 )
CALL VCHAR( 10 10 42 10 )
And behold the awesomeness. Note the spaces: The space between the open parenthesis of HCHAR( and VCHAR( is essential. The spaces between the parameters are just normal for Forth (and actually looks much nicer than using commas). The space before the final closing parenthesis is also required.
You just changed Forth to be more like BASIC.
You don't have to have numbers in the parameter list. They can be words or expressions:
10 CONSTANT TEN
42 CONSTANT FORTY-TWO
CALL HCHAR( TEN TEN FORTY-TWO  TEN TEN + )
(That last phrase: TEN TEN + puts 10 on the stack, then another 10 on the stack, then + ("add") removes them and replaces them with their sum, thus leaving the repeat count for HCHAR)
So how does it work?
Let's look at CALL first.
CALL does nothing. It's only job is to be there to make those more familiar with BASIC happy. It has no code in it; it's empty. Furthermore, it's what is known as an IMMEDIATE word, meaning that it executes DURING COMPILATION, not during execution.
An example:
: FRED ( -- ) CR ." I AM FRED" CR ;
Type that in. Nothing much happens. The word FRED gets stored in the dictionary ready to be used. Now type FRED and press enter. FRED executes. No big deal.
Now, type this:
: BOB ( -- ) CR ." BOO! BOB WAS HERE!" CR ; IMMEDIATE
Okay, you typed it in. Nothing much happened. Now execute it: type BOB and press enter. Again, no big surprises.
Now, try this:
: TOM ( -- ) BOB CR ." HELLO! I AM TOM!" CR ;
Did you see what happened? While *TOM* was being compiled, BOB got in on the act and ran, rather rudely announcing his presence.
So what happens if we run TOM (type TOM and press enter)?
HELLO! I AM TOM!
Where's BOB? Should BOB not also say BOO!? No. And the reason why is very clever and is the secret sauce that makes Forth so very powerful.
Here it is:
"Immediate words execute at compile time."
That is, immediate words execute when a word is being compiled, *not* when the compiled word is executed! That's possibly a brain-hurting statement. Consider this TI BASIC code:
10 CALL HCHAR(10,10,42,10)
Now, when you press enter, the TI BASIC compiler switches on and compiles your code into some internal magic code that will do what you want it to do when you later RUN it. Okay. All normal stuff. But consider this: When the TI BASIC compiler is compiling that line of code, it does so entirely privately. No have no control over what compiler does. Mind your own business, it's nothing to do with you. The compiler privately compiles that code (or doesn't if there's an error), and you are just a bystander.
That's not the case in Forth. In Forth, you can use "immediate" words that run when the compiler is compiling. And because they run when the compiler is compiling, you can "hijack" the compiling process, and do something: make a fart sound; say something on the speech synth; load a file; anything you want.
You can even compile your own code.
Think about that.
Code that compiles code.
And THAT is what makes Forth so powerful.
So, lets get back to TOM and examine what happened. In Forth, the compiler is switched on by : (colon) and switched off by ; (semi-colon):
: SOME-WORD <CODE GOES HERE> ;
The compiler just walks along the line of text, and when it sees a word it looks for it in its dictionary and if it finds it, it compiles a call (like a GOSUB) to it. Now you can see why spaces are so critical in Forth. They are what separate the words so that they can be found in the dictionary.
However, when the compiler is looking for a word, if it finds it, it checks to see if it is immediate or not. If it is not, it just compiles a call/GOSUB to the word. However, if it *is* immediate, it *executes* it, and does not compile it.
That means you can put a reference to an immediate word in your definition, and at that point in the compilation process it will call your immediate word, and *you* can do something to the word that is *currently* being compiled, like add some more code to it. When the immediate word ends, the compiler just carries on compiling, totally oblivious to anything you may or may not have done to the word currently being compiled. It's none of its business. It's your business. You are in total control.
Thus when TOM was being *compiled* the compiler saw the reference to the word BOB and saw that it was "an immediate word" and so it executed BOB, and BOB did it's thing (in this case, writing a cheeky message to the screen) and then carried on with the compiling.
Now you understand why, when TOM was *executed*, there was no message from BOB. BOB did it's thing while TOM was being *compiled*.
Yes. In Forth, there are two distinct excecution phases:
  • Run-time: When a word is just plain excecuting, doing its thang;
  • Compile time: When a word is being compiled.

And you can do whatever the hell you want in either phase.
You might want to go for a little lie down at this point!
Now, lets look at HCHAR( and see what it does:
: HCHAR( 
  ASCII ) WORD EVALUATE
  STATE @ IF COMPILE HCHAR ELSE HCHAR THEN ; IMMEDIATE
When the compiler sees HCHAR( it sees that it is immediate and so it executes it. The first thing is does is place the ASCII code for a ) (closed parenthesis) on the stack. Then WORD executes. WORD reads the line of text and will stop when it sees the ) character. So, if you typed CALL HCHAR( 1 2 42 4 ) WORD would capture 1 2 42 4
The output of WORD is two numbers: The address and length of the text that it found. This is fed into EVALUATE that simply evaluates the string as if it were a line of code entered at the keyboard. In this case, 1 2 42 4, or TEN TEN 42 TEN etc. are all valid code, so it executes it according to the rules of Forth:
  • If we're compiling (i.e. the compiler was switched on with : (so we're building a word) then it will compile what it sees;
  • If we're not compiling, it will just execute what it sees there and then, just like in BASIC when you enter something without a line number.
So, we're using EVALUATE to evaluate the parameters for us between the HCHAR( word and the closing ) character. Note the cheeky use of the open parenthesis in HCHAR( which makes it look like some part of the the syntax of the word, but it isn't: It's just part of the name! And note also the closing parenthesis which again looks like syntax but is in fact nothing more than a marker for WORD to look for to isolate the parameters so that it can feed them into EVALUATE.
The magic of Forth.
The last bit of HCHAR( is very simple indeed. It just looks to see if we're in compile mode (the variable STATE will be 0 if we're not compiling, and >0 if we are compiling). If we ARE compiling, we compile a call to HCHAR (the original version of HCHAR built into the TurboForth EPROM). See? We're "injecting" code into the definition that is being compiled. However, if we're NOT compiling, we just execute HCHAR right there and then, which uses the parameters that EVALUATE evaluated for us.
Thus we can do:
CALL HCHAR( 1 2 42 99 )
(i.e. not in a definition, so it will execute immediately, like BASIC code with no line number)
Or
: LINE ( -- ) CALL HCHAR( 1 2 42 10 ) ;
And both will work fine and do what they're supposed/expected to do.
So, again, here's what happens when that LINE defintion above is compiled:
  • The compilier sees that CALL is an immediate word, so it runs it. CALL actually does precisely nothing, it compiles nothing and runs nothing. It has 0 impact on run-time speed. It's purely "syntactic sugar" to sweeten things up for BASIC lovers. It's a total sham. You don't need to use it at all.
  • The compiler sees that HCHAR( is immediate so it runs it.
  • HCHAR( temporarily takes over, and reads the input up to the closing parenthesis and evaluates them.
  • Since we're building a definition (LINE) the compiler is ON, so EVALUATE will compile them (by calling a new instance of the compiler and saying "HEY! Compile this! Thanks man!" (How's that for a mind f**k!?).
  • HCHAR( then exits, it's done it's thing.
  • Control now goes back to the compiler. The compiler only sees ; (semi-colon) because the parameters were consumed by WORD and EVALUATE so it completes the definition and we're done.
If you were to disassemble the definition of LINE what you would see is this:
1 2 42 10 HCHAR
In other words, HCHAR( re-arranged the code so that the parameters went first, then called a reference to the internal (in the EPROM) HCHAR which expects the parameters to be on the stack.
The whole HCHAR( definition is nothing more than a trick which allows us to put the parameters *after* HCHAR( but internally it compiles HCHAR after the parameters.
And that is the power of Forth. If you don't like:
ROW COLUMN CHAR REPEATS HCHAR
You can make your own word to give you:
CALL HCHAR( ROW COLUMN CHAR REPEATS )
Or any other combination.
And there endeth the lesson.
This is without a doubt a bit of mind melter when you are new to Forth, so don't worry if you don't understand it all. I just wanted to give you an appreciation of the power and flexibility of Forth. It's not essential to understand this stuff right now.
And now a quick demo using our new words. We haven't covered a lot of the code below yet. For now, just sit back and enjoy.
: FWD-BOX ( -- )
  30 0 DO
    12 0 DO
      CALL VCHAR( I I   I 33 + J +   24 I 2* - )
      CALL VCHAR( I   31 I -   I 33 + J +   24 I 2* - )
      CALL HCHAR( I I   I 33 + J +   32 I 2* - )
      CALL HCHAR( 23 I -   I   I 33 + J +   32 I 2* - )
    LOOP 
  LOOP ;
 
: REV-BOX ( -- )
  0 29 DO
    12 0 DO
      CALL VCHAR( I I   I 33 + J +   24 I 2* - )
      CALL VCHAR( I   31 I -   I 33 + J +   24 I 2* - )
      CALL HCHAR( I I   I 33 + J +   32 I 2* - )
      CALL HCHAR( 23 I -   I   I 33 + J +   32 I 2* - )
    LOOP 
  -1 +LOOP ;
 
: BOXES ( -- ) \ top-level - run me
  1 GMODE 
  5 0 DO FWD-BOX REV-BOX LOOP
  0 GMODE ." Thanks for watching!" CR ;
 
Note the additional spaces in the paremeters so that it's easier to identify each paremeter.
References:
Edited by Willsy
  • Like 4
Link to comment
Share on other sites

So, I'm really really really bad at memorizing things. Good at understanding and comprehension, but awful at memorization.

 

This write up is great! It addresses/explains one of the things I find creeping into the Forth vocabularies, that I dislike the most: prefix notation.

 

Some things, it seems end up this way because the string library wasn't available yet. I've seen infix too, and it always trips me up.

 

While the language is super powerful, using it this way drives me nuts! :) just my opinion...

 

I do remember BITD, seeing an implementation of LOGO in TI-Forth. That I assume used the same mechanism. This does really show how powerful, linguistically, the language is.

 

-M@

Link to comment
Share on other sites

Yes, it's cool eh? Infix is prefix - we're just brought up to read prefix. We live in an infix world.

 

I had a thought earlier: It wouldn't be too hard to change the code above such that you could include commas to separate the parameters. The parameters themselves are still prefix Forth, but you can visually separate them with commas.

 

Even I was surprised at how easy it was:

: CALL ( -- ) ; IMMEDIATE \ do absolutely nothing
 
: ,PARAM ( -- )
    ascii , word evaluate ;
 
: )PARAM ( -- )
    ascii ) word evaluate ;
 
: HCHAR( 
  ,PARAM ,PARAM ,PARAM )PARAM
  STATE @ IF COMPILE HCHAR ELSE HCHAR THEN ; IMMEDIATE
  
: VCHAR( 
  ,PARAM ,PARAM ,PARAM )PARAM
  STATE @ IF COMPILE VCHAR ELSE VCHAR THEN ; IMMEDIATE
Now you can do:
10 CONSTANT TEN
42 CONSTANT FORTY-TWO
CALL HCHAR( TEN, TEN, FORTY-TWO,  TEN TEN + )
Note the commas :D
The demo becomes:

: FWD-BOX ( -- )
  30 0 DO
    12 0 DO
      CALL VCHAR( I, I, I 33 + J +, 24 I 2* - )
      CALL VCHAR( I, 31 I -, I 33 + J +, 24 I 2* - )
      CALL HCHAR( I, I, I 33 + J +, 32 I 2* - )
      CALL HCHAR( 23 I -, I, I 33 + J +, 32 I 2* - )
    LOOP 
  LOOP ;
 
: REV-BOX ( -- )
  0 29 DO
    12 0 DO
      CALL VCHAR( I, I, I 33 + J +, 24 I 2* - )
      CALL VCHAR( I, 31 I -, I 33 + J +, 24 I 2* - )
      CALL HCHAR( I, I, I 33 + J +, 32 I 2* - )
      CALL HCHAR( 23 I -, I, I 33 + J +, 32 I 2* - )
    LOOP 
  -1 +LOOP ;
 
: BOXES ( -- ) \ top-level - run me
  1 GMODE 
  5 0 DO FWD-BOX REV-BOX LOOP
  0 GMODE ." Thanks for watching!" CR ;
How cool is THAT? :P

 

  • Like 1
Link to comment
Share on other sites

Yes, it's cool eh? Infix is prefix - we're just brought up to read prefix. We live in an infix world.

 

I had a thought earlier: It wouldn't be too hard to change the code above such that you could include commas to separate the parameters. The parameters themselves are still prefix Forth, but you can visually separate them with commas. ...

 

I hesitated posting this because I do not want to detract from this wonderful thread, but I must call you on this one, @Willsy.

 

Describing math notation as prefix (Polish), infix (algebraic) or postfix (reverse Polish) has to do with the position of the operator, not the parameters. Forth is, indeed, postfix, with the operator last.

 

...lee

  • Like 2
Link to comment
Share on other sites

We do live in an infix world, but I program to escape that :)

 

---

 

Now, what would be cool.. (in addition to the linguistic gymnastics) would be to write a TidBit compiler in forth.

TidBit has taken the line numbers away...

 

I imagine creating compiling words this way makes the compiler a little slower, but the runtime execution doesn't look like there is any inherent reason it should be any slower.

 

-M@

  • Like 1
Link to comment
Share on other sites

...

I do remember BITD, seeing an implementation of LOGO in TI-Forth. That I assume used the same mechanism. This does really show how powerful, linguistically, the language is.

 

-M@

 

I have had on the back burner for a long time a project to implement TI Logo in Forth, mainly to better manage the "ink" supply and to add file support. I was unaware that a TI Forth implementation existed.

 

...lee

Link to comment
Share on other sites

 

I have had on the back burner for a long time a project to implement TI Logo in Forth, mainly to better manage the "ink" supply and to add file support. I was unaware that a TI Forth implementation existed.

 

...lee

 

Uh oh... if you are not aware of it, then it is probably lost... I wonder, if it simply wasn't advertised as being written in Forth. But it was clearly Forth. You loaded Forth, then loaded Logo. It might not have called itself Logo...

 

-M@

Link to comment
Share on other sites

Uh oh... if you are not aware of it, then it is probably lost... I wonder, if it simply wasn't advertised as being written in Forth. But it was clearly Forth. You loaded Forth, then loaded Logo. It might not have called itself Logo...

 

-M@

 

Hah! You give me too much credit. It is probably a matter of focus. When I latched onto TI Forth in the mid 80s, I was far more interested in what I could do with Forth than what others had done.

 

...lee

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...