frustration/frustration.4th
2022-05-21 22:44:50 -07:00

170 lines
5.3 KiB
Forth

: rdup r> r> dup >r >r >r ;
: rdrop r> r> drop >r ;
: loop[ [ ' rdup lit ] , [ ' rdrop lit ] , ; immediate
: ]loop latest @ 8 + , ; immediate
: ( loop[ key 41 = ? ret ]loop ; immediate
( -------------------------------------------------------------------------
Phew!
OK, what we just did was add comments to our Forth.
The ( word starts a comment. The close-parenthesis ends a comment.
Remember that words are whitespace-delimited, so you need to put whitespace
after the ( so it is recognized as a word. You can't just launch straight
into writing your comment. That's why the start of this comment said,
( ----
and not,
(----
If you don't type a close-parenthesis you can have a multi-line comment,
like we are doing now.
In the few lines starting this file, we also gave ourself a better way of
writing loops, so now we don't have to muck around with the "rdrop ret"
return-from-caller trick quite so much!
Let's explain how it works.
LINE 1 --> : rdup r> r> dup >r >r >r ;
LINE 2 --> : rdrop r> r> drop >r ;
LINE 3 --> : loop[ [ ' setup lit ] , [ ' rdrop lit ] , ; immediate
LINE 4 --> : ]loop latest @ 8 + , ; immediate
Line 1-4 are defining a new loop construct called loop[ ... ]loop that
makes a word tail-recursive.
We already talked about "tail call optimization" in frustration.rs, when we
hand-made a very bad version of it. Now it's time to use Forth to make a
better one that is less annoying to use.
Let's lay out a tail-recursive word in memory like this:
[ Link field ]
[ Name field ]
[ --- Start of code field --- ]
[ Subroutine call to duplicate the return address ] <--- A
[ Subroutine call to drop the return address ] <--- B
... Your code goes here ...
[ Subroutine call to B ]
[ RET ] <---------------------------------------------- C
The loop[ word compiles the two subroutine calls at the start of the code field.
The ]loop word compiles the one subroutine call at the end of the word.
Later we will talk about how they managed to do that.
The rdup word is the "subroutine to duplicate the return address".
The rdrop word is the "subroutine to drop the return address".
Here is why it works.
Start of tail-recursive subroutine: Return-stack: { caller }
After calling A: Return-stack: { caller caller }
After calling B: Return-stack: { caller }
In other words, the back-to-back calls to A and B cancel each other out.
In the body of the loop: Return-stack: { caller }
Meaning you can break out of the loop with RET.
At subroutine call to B: Return-stack: { caller C }
After calling B: Return-stack: { caller }
And now we are at the top of the loop again.
So values are not building up on the return stack forever, and
we have got the tail recursive behavior we want, hooray!
How the loop[ and ]loop words work:
There is a pattern used in this code that is probably unfamiliar to
you:
[ ' xyz lit ] ,
What this does is:
[ starts interpreting
' xyz looks up the address of word xyz
lit emits code that pushes the address to the stack
] starts compiling
, takes the address of xyz, which is now on the stack, and
compiles a subroutine call to xyz
Put another way, typing out:
: example [ ' xyz lit ] , ; immediate
actually compiles:
: example 12340 , ; immediate
{where 12340 is the address of xyz.}
except Forth looked up that address for you, so you didn't have to
hard-code 12340.
And then, because example is an immediate-word,
typing out:
: example-2 example ;
actually compiles:
: example-2 xyz ;
Going back to the real code that we just wrote,
typing out:
: loop[ [ ' rdup lit ] , [ ' rdrop lit ] , ; immediate
actually compiles:
: loop[ AAAAA , BBBBB , ; immediate
{where AAAAA is the address of rdup and
BBBBB is the address of rdrop}
which means that typing out:
loop[
actually compiles:
rdup rdrop
As an end result, we now have a rather nice way of writing loops, that
doesn't involve the programmer manually fiddling with the return stack
any more.
Forth interpreting mode and immediate-words are very powerful features.
This is how a small ~1 KB language core can extend itself into a "problem
oriented language" in a short amount of time.
Finally we use the tail-recursion feature we just added to the language,
to add comments to the language.
LINE 5 --> : ( loop[ key 41 = ? ret ]loop ; immediate
: ( Start defining a word called (
loop[ Make this a tail-recursive word
key Read a key code from stdin
41 = ? ret If it is close-parenthesis, we're done
]loop Otherwise, loop again
; immediate This will be an immediate-word, so it
executes immediately when it is seen.
------------------------------------------------------------------------- )
( Here is a riddle for you )
: :noname ( -- xt ) here @ [ ' ] , ] ; immediate
: stars ( n -- ) loop[ dup 0= ? [ , ] 1 - 42 emit ]loop ;
( How does the above code work?
You can run "stars" as follows:
Input: 8 stars
Result: ********
Input: 36 stars
Result: ************************************
)