codex/resources/txt/starting-forth/starting-forth.org

28 KiB
Raw Blame History

#+Starting Forth

This file contains my notes reading the book Starting Forth by Leo Brodie. In this directory are also several files containing Forth source code. These are my solutions to the problems found in the book.

Chapter 1

Some operations defined in this chapter

  • spaces - ( n ) - prints n spaces ('15 spaces' will print 15 spaces to the screen).
  • emit - ( c ) - prints a number as an ascii character to the screen.
  • xxx yyy ; - ( -- ) - define new words like this: : star  42 emit ;
    
  • cr - ( ) - print a carriage return to the screen (newline).
  • ." <text> " - ( ) - print the string <text> to the screen.
  • . - ( n ) - pop a number off the stack and print it.
  • Forth features arithmetic with +, -, / and *.

The dictionary

Forth has a "dictionary" where all the word definitions are stored (words = functions). When you create a new word with : and ;, the definition is stored in the dictionary under the given name.

The text interpreter

When a word is entered, Forth will "activate a word called INTERPRET" which will look up that word in the dictionary. If it finds a definition, it will pass it onto another word called "EXECUTE" which will perform the action. Otherwise, it will give it to the numbers-runner called "NUMBER" which will push the word onto the stack if it is indeed a number. Otherwise, the interpreter will throw an error. It's interesting that all the parts of the compilation and interpretation process are described as simple words that are part of the Forth system, I wonder if these are redefinable.

Stack notation

Stack notation is used to communicate the effects a function has on the stack. The basic form is like this: ( before after ) The left side indicates what should be on the stack before you execute a word, the right side indicates the things that will be on the stack afterwards. The stack notation for +, for example, is ( n1 n2 sum ). The rightmost item on the left side is the top of the stack (in the previous example, this means that n2 is on top of the stack).

General thoughts

The end of the chapter features a review of terminology and the words defined so far.

Chapter 2

General notes

This chapter is mostly about stack based arithmetic and a few stack operations. These are documented tersely and well in the book, so I won't write about them here.

The non-desktructive stack print (.s) is already provided with gforth, but that definition looks very inviting. I'm excited for when I can understand it.

I also find it's a bit difficult to decide on the proper stack layout for a function. I guess it will come for practice (or pracicality; when you chain multiple words together, I'm guessing there will become a preferred stack order).

I find it weird to use /; I often forget which argument is the dividend and which is the divisor. - is more straight forward, weirdly.

"Problems"

  1. The difference between 'dup dup' and '2dup' is that the former will produce two copies of the value on top of the stack, while the latter will produce a copy of both the second value of the stack as well as the first.
  2. 4reverse  swap 2swap swap ;
    
  3. 3dup  dup 2over rot ;
    
  4. I start by factoring the expression like so: a² + ab + c = a(a + b) + c Then, the answer is trivial.
  ( c a b -- result )
  : answer  over + * + ;
  ( a b -- result )
  : answer  2dup - rot rot + / ;
  : homicide  20 + ;
  : arson  10 + ;
  : bookmaking  2 + ;
  : tax-evasion  5 + ;

  : convicted-of 0 ;

  : will-serve  . ." years" ;
  ( eggs -- )
  : egg.cartons  6 /mod
                 cr . ." carton(s), with " . ." left-over egg(s).";

Chapter 3

General notes

The chapter starts off by talking about how redefining words doesn't actually erase the previous definition, but adds a new entry at the "back of the dictionary". This means you can use FORGET to remove the newest version of the word and get back the old definition. I learned this when I made the forth interpreter for Pansy Linux; the dictionary is a reversed linked list with this very property.

The rest of the chapter describes the Forth file system as well as the editor. Neither of these are available on modern systems, but it might be fun to re-read this chapter for ideas if I ever want to create a virtual machine operating system.

Forget

As it turns out, gforth doesn't have forget! I'm switching implementation…

Looks like most implementations don't implement forget. pforth is the only one I found so far… I guess I'll use that then?

NOTE: Forget not only erases the newest version of the word provided, it also removes all other words defined after that word.

The block system

The book describes how you can interface with files from the Forth system. Source files can be stored in "blocks" of 16 lines with 64 characters on each line (1024 bytes). To load, say, block 12, you type '12 load' at the interpreter.

This sounds very neat and facilitates Forth as a system in addition to just a programming language (and it sounds pretty aesthetic~. I know CollapseOS adopted the block model from Forth).

Style

There's some style tips here as well:

  1. Separate the name of a word definition from the contents by three spaces.
  2. Break definitions up into phrases, separated by double spaces.
  3. If the definition takes more than one line, indent all but the first line.
  4. Don't put more than one definition on a single line unless the definitions are very short and logically related.

Handy word (just one!)

depth ( n ) - Places the number of items on the stack onto the stack.

Chapter 4

General notes

This chapter is about branching (if/else). It starts with number comparison and the basic 'if' form which is like follows:

  if <consequent> then <rest of the program>

The part after 'then' will always be executed. Special comparison words like = will push a true value (which is -1 by the way) on the stack, which will be popped off by the conditional check.

If can also take an else branch:

  if <consequent> else <alternative> then <rest of the program>

The if form can only be used in a word definition, for some reason. Weird.

Not reverses the boolean on the stack.

If's can be nested, but you'll end up having to close off each if with a respective then. This leads to weird situations ilke if <something> else if <something else> else … then then then then then ;

Turns out, any value other than 0 counts as true (like in C).

This means the logical or operator in Forth is simply +! Ah, but what if the values are each other's inverse? If you add -1 and 1 you get 0, which is the wrong answer. Because of this, Forth has the or operator which works like you'd expect.

New words

Words with built-in if

?dup - dup only if the argument is non-zero. abort - abort execution and return to the interpreter, clearing the stack in the process. The book mentions ?stack for checking stack underflows, but it doesn't exist in my implementation (fforth). I guess it can be implemented with depth and 0=.

Logical operators

(self explanatory) and or

Comparing numbers

(these are self explanatory)

Two arguments

= < >

One argument

0= - this one is actually equivalent to not, but may be used for readability. 0< 0>

"Problems"

  1. Note - My implementation uses -1 as true, instead of 1 as the book.

    1. 1 (not not 1 = -1)
    2. 0 (not not 0 = 0)
    3. 1 (not not 200 = not 0 = -1)
  2. Huh?
  : card   17 > if ." Alcoholic beverages permitted" else ." Under age" then ;
  : sign.test   dup 0= if ." ZERO" else
                dup 0< if ." NEGATIVE" else
                         ." POSITIVE" then then drop ;
  1. Simply wrapping the loop in an if statement works!
  : stars  dup if 0 do 42 emit loop then ;
  1. Probably not a good solution, but it works well.
  ( n low-limit hi-limit -- )
  : within rot  ( l h n )
           swap ( l n h )
           over ( l n h n )
           swap ( l n n h )
           <    ( l n t/f )
           rot rot  ( t/f l n )
           <=
           and
  ;

  ( The version provided by the book is much nicer )
  : <rot  rot rot ;
  : within <rot over > not <rot > and ;
  ( But my implementation doesn't have not )
    : guess  2dup = if ." CORRECT" drop else
             2dup < if ." TOO HIGH" else
                     ." TOO LOW" then then drop ;
  : n= over = ;

  : speller dup if
                dup 0< if ." NEGATIVE " then
              abs 1 n= if ." ONE" else
                  2 n= if ." TWO" else
                  3 n= if ." THREE" else
                  4 n= if ." FOUR" else
                       ." OUT OF RANGE" then then then then
            else ." ZERO" then drop ;
  ( TODO )

Chapter 5

General notes

This chapter starts off by introducing a few new arithmetic words and the return stack along with a few words for manipulating this. The return stack looks really powerful for easing the stack manipulation bit of Forth.

One thing to note is that the return stack must be empty before the end of a word definition.

The chapter also describes how to deal with floating point numbers in Forth. Don't. Instead, use fixed-point numbers but externally represent them as floating points. Generally, instead of thinking of expressions like (x / y) * z, translate them into (x * z) / y for greater accuracy (which uses the */ word in Forth). In general, one can use the expression 3 2 */ to mean rational numbers (3/2 in this case) which you can multiply some other fixed-point number.

New words

Arithmetic

These are self explanatory. 1+ 1- 2+ 2- 2* 2/

Miscellaneous math

abs ( n |n| ) negate ( n -n ) min ( n1 n2 n-min ) max ( n1 n2 n-max )

Return stack manipulation

>R ( n ) - Pops the first value off the stack and pushes it to the parameter stack. R> ( n ) - The opposite I ( n ) - Copies the first item off the return stack I' ( n ) - " second " J ( n ) - " third "

Scaling operators

*/ ( x y z x*y/z ) - This is a word which uses a 32-bit integer as the intermediate result of mutliplying x by y. This makes it better to use for scaling than simply * and / alone. /mod ( x y z remainder ) - The same as /, but returns the remainder rather than the quotient.

Chapter 6

General thoughts

This chapter is all about looping!

Handy hint

Typing in an unrecognised word will clear the stack since the interpreter will call abort.

Making quit the last word of a word definition will silence the "ok."

Looping constructs

Definite loops do…loop

This is your basic for loop in other languages. The syntax is the following: limit index do … loop So you put the limit first, then the starting index, then do, the body and finally loop. The do loop works by first pushing the limit and the index onto the return stack and incrementing the index until it is the same as the limit. This means you can use the i word to get a copy of the index at any given moment in the loop! Since we can nest loops, the words i and j make a bit more sense. You can use j to access the index of the outer loop and i to access the index of the inner one.

for (int j = 0; j < 10; j++) for (int i = 0; i < 10; i++) …

Looks familiar?

+loop can make the index go up by a specific increment. It pops a value off the stack and increments the index by that number.

Do loops terminate when (going up) the index reaches or passes the limit and (when going down) it passes the limit. A do loop always executes at least once.

One can use the word leave to set the limit to equal the index, so that the loop will immediately terminate on the next time loop is executed. This can be useful for early exits.

Indefinite loops

Indefinite loops are like a do while loop, they loop if a condition is true by the end of the body execution.

The basic indefinite loop is on the form:

begin xxx f until

Where xxx is the body of the loop and f is the boolean flag indicating if the loop should, well, loop.

A simple infinite loop can then be defined like so:

begin xxx 0 until

There's another form of the loop as well, with syntax like this:

begin xxx f while yyy repeat

This one's weird. It performs xxx, then checks for a boolean true. If it's true, it performs yyy and repeats again. The begin…until loop will loop on the opposite condition. That is, it will only loop if the condition is false.

Chapter 7

General notes

This chapter is about number representations, like unsigned, double width, hexadecimal etc.

Turns out that the and and or words defined earlier are actually bitwise operators. They work fine as logical and/or regardless.

Double length numbers

To push a double length number onto the stack, use punctuation in the number. Punctuation works with , and . and a few other characters. So 4.000.000.000.000.000.000.000 pushes that number as two cells.

Number formatting

One can create number formatting words with the special words <# and #>. The book has good sections on this, I won't bother trying to word them myself here. NOTE! These only work on unsigned, double length words. To make them work on single length words, put a 0 on the stack before calling it.

This is a sort of DSL, kinda like format in Common Lisp. I wonder if there are more of these in Forth, and how easy it is to make one…

New words

Unsigned number manipulation

The numbers on the stack are both unsigned and signed at the same time. The programmer decides what representation they are by the operations performed on them. Most arithmetic operations have an unsigned equivalent:

  1. ( u ) Prints the unsigned number u.

u*, u/mod, u< does what you'd expect. do … /loop is your normal loop, but with unsigned index, limit and increment. It takes an increment value, like +loop.

Changing the base

One can change the base of the program with the words hex (hexadecimal), octal and decimal. You can also change the base to whatever you please with 'n base !'. That means you can declare : binary 2 base ! ; and see numbers in binary form! That's pretty cool.

Double length numbers

  1. ( d ) - Prints a signed, double length number.

d+, d-, dnegate, dabs, dmax, dmin, d=, d0=, d<, du< do what you'd expect. d.r ( d width ) - Print the signed 32 bit number, right justified within the field width.

Chapter 8

General notes

This chapter is called "Variables, constants and arrays". My normie programming brain thinks these are good things, and will hopefully make the code a bit more readable?

Variables

You declare a variable like so:

variable <var>

One can set the variable like so:

<value> <var> !

This means that the earlier base changing trick (<num> base !) is actually just changing a global "base" variable!

! is pronounced "store".

@ is the opposite of !, and is used for getting the values stored in a variable. It's pronounced "fetch" and works thusly:

<variable> @

Printing the value of a variable (<var> @ .) is so common that Forth has a separate word for it, ?.

Since variables are accessed at run time, it's good to store often changed values there instead of in words since words that use those words would have to be recompiled to work with the new definition. Variable declaration is similar to word declaration, but it compiles a new word which when called pushes its value address onto the stack. That's how <value> <var> ! works, it pushes a value, pushes an address, and stores that value into that address.

Constants

Constants are declared like this:

<value> constant <name>

The value can be retrieved by executing the constant name.

Arrays

Arrays are simply variables with extra memory. What does that mean? To make an array called "arr", you do this:

variable arr 8 allot

The "8 allot" part means that we want arr to contain 8 additional bytes, that is, 1 extra cell on modern machines.

The new fields can be accessed with simple pointer arithmetic.

2 arr 8 + ! - Sets the second element of the array to 2 (on a 64-bit machine).

You can make an array with initial elements using create.

create xs 1 , 2 , 3 ,

will create an array called xs with 1, 2 and 3 as its initial elements. They are each a cell big. Create creates an array which can be manipulated as normal (the book has a notice for polyFORTH, but I don't think it's relevant).

Byte arrays

In addition to arrays, Forth also has byte arrays which operate on bytes instead of cells. I think this one's probably better for portability since you don't have to consider the cell size on different systems when using it. You also don't have to double the index of an array since each value correspond to a single byte.

Byte arrays can also be created with initial values, like you can for normal arrays with create. The difference is that you use c, instead of ,.

create bytes 1 c, 2 c, 3 c,

New words

!, @ and ? are described in the variables section. +! ( n adr ) - Increments the value at adr with n. Constant and variable operations can be prefixed with a 2 to mean a double-length variable/constant. fill ( adr n b ) - Fill n bytes of memory beginning at the address with value b. erase ( adr n ) - Like '<adr> <n> 0 fill' c! and c@ are like ! and @ but for byte values.

Chapter 9

General notes

This looks to be a very interesting chapter. It's all about how Forth works internally, which is extremely captivating.

In the section "vectored execution" the book goes over a powerful strategy. Since the ' word gives the address of a word, you can store it in a variable. Then, later, you can execute this word with <var> @ execute. This means you can have a word have different meanings in different parts of the program! This makes it possible to compile words and place them into variables, which bypasses the need for recompiling every word that uses that word.

Dictionary entries

Dictionary entries are described in the book. They have the following structure:

Name Link (to previous word, the dictionary is a backwards linked list) Code pointer Parameter field

The code pointer is what makes variables, constants and colon definitions different. Variables have a code pointer pointing to code which pushes the variable's address onto the stack, and constants have a similar one but pushes a value rather than an address. For other words, it points to machine code to be executed when the word is invoked.

The parameter field contains data, and the size of it depends on the type of variable, constant or colond definition. Normal variables/constants have a 1 cell sized parameter field, 2constant/2variable declares a constant/variable with a 2 cell sized parameter field and colon definitions can have arbitrarily long parameter fields. The address provided by tick and used by execute is the parameter field address (pfa).

The name and the link is called the 'header' and the code pointer and the parameter field is called the 'body'.

In colon defined words, the parameter field contaiins the addresses of the words that comprise the defintion. These addresses are called code field addresses.

How colon defined words work

At the end of any colon defined word sits a call to exit. When executing a word, the interpreter will first push the current interpreter pointer to the return stack. Exit will pop this address and return to it. Therefore, you can change where a colon defined word will return to by manipulating the return stack directly! This also means that you can use exit as an 'early return' to a word.

The pad

The pad is some memory location a fixed place after here which is used for text processing. Since it starts a fixed point after here, it moves as definitions are added.

The parameter stack

The parameter stack (remember, this is the thing referred to as just "the stack") resides far above the pad in memory. The stack is in reality just memory with a pointer that changes as we push and pop items off it. The stack grows downwards, that is it begins at high memory and grows towards low memory. The stack pointer memory value can be fetched with 's. The current top value of the stack is therefore the same as 's @ (which is the same as dup!). The point right before the bottom of the stack is pointed to by s0 (which doesn't seem to be defined in gforth!). 's, s0 and h are not required by the standard, which explains why they're not available in gforth.

Input buffer

The text retrieved from the command line interface is stored in the input message buffer, which is located just above the stack and grows upwards.

Return stack

Above the input buffer is the return stack. It works the same as the parameter stack, but doesn't have words like 's and s0 defined for it.

User variables

Variables available to the user (like base) is stored above the return stack. User variables are different from the ones defined by the user (with variable). They're kept in an array called the "user table". This was probably more relevant back in the day with multi-user systems, but each user has their own user table, meaning that if one person changes a base, it won't affect others.

Vocabularies

Vocabularies work as namespaces for word definitions. The book describes three vocabularies, the standard Forth vocabulary, editor (not present on modern systems) and assembly (which allows for making words in the computers assembly language??). Now I want to know how to use this assembly vocabulary. Vocabularies are just a part of the dictionary, but words of particular vocabularies only point to words of the same vocabulary in the dictionary. The current vocabulary is called the 'context' (it can be accessed with the word context, but it's just an address). The current vocabulary new words are linked to is found in the variable current. To change current, use the word definitions (which will take the current context and put it in current).

New words

' ( adr ) - Takes the next word of the input stream and pushes the address onto the stack. Since ' takes the next word in the input stream you can do stuff like

set  ' <var> ! ;

And invoke it like "set <word>". The input stream is visible, even to words.

dump ( adr n ) - Prints the first n bytes of data found at adr.

['] - Takes the next word inside the definition and puts the address on the stack. It ignores the input stream, which will be handy.

h ( adr ) - Returns the current address in the dictionary. This increases as more words are defined.

here ( n ) - The same as h @. You can use this to see how much memory something takes by first performing here, defining something and comparing the new value of here to the one on the stack.

, is explained to be the same as 'here ! 2 allot'

user - define a new user variable.

Chapter 10

General notes

Ah, a practical chapter (it's about i/o).

It starts off with some (now) irrelevant information about blocks.

It's a difficult chapter to read, since half the information is in this way irrelevant.

"New" words (some old)

Output

emit ( c ) - Prints a value on the stack as an ascii character (using the lower byte).

type ( adr n ) - Prints n bytes starting at adr. Used for printing strings.

Input

key ( c ) - Waits for a key to be pressed, then pushes that ascii value onto the stack. NOTE: This works pretty bad in emacs since it doesn't support terminal codes that well.

expect ( adr u ) - Reads u bytes as a string and stores it at adr.

word ( c adr ) - Read a word from the input, using c as a delimiter. The address returned has the count as the first value and the string follows.

text ( c ) - Reads a string using c as a delimiter, but places the string in the pad. Text is non-standard and doesn't exist in gforth.

query ( ) - Expect, but read 80 chars and store them in the input message buffer. The input has to be valid character.

Memory manipulation

move ( adr1 adr2 u ) - Copies u cells from adr1 to adr2.

cmove ( adr1 adr2 u ) - Like move, but for bytes instead of cells.

String operations

count ( adr adr+1 u ) - Takes the address of a string and returns the length of the string (u) as well as the incremented address.

-text ( adr1 u adr2 f ) - Compares two strings of length u (adr1 and adr2).

Chapter 11

General notes

What is this chapter about? Metaprogramming? The chapter name is "extending the compiler" which makes my Lisp brain tingle.

Compile time and run time

One of the first things the chapter tackles is the difference between run time and compile time. You see, some words have both run time behaviour and compile time behaviour. These types of words fall in one of two categories; defining words and compiling words.

Constant is a defining word, as it takes an identifier at compile time and exhibits run time behaviour in that, every time you call that word, you get a value on the stack.

A compiling word is a word used inside a colon definition which does different stuff when the word is defined from what is shown at run time. For example, ." will at compile time embed the string into the word's parameter field, but at run time print the string. Other examples include if and loop.

The first example of metaprogramming uses create and does>. I'm not really seeing the power yet, and it still looks a bit limiting.

The next section covers how to change the behaviour of the colon compiler. Now we're talking! Any word can be made immediate (word that will be executed at definition time) by simply placing the immediate word behind a definition.

say-hello   ." Hello " ; immediaten

Now, if this word is placed in a colon definition, it will execute at compile time rather than run time. It can still be executed interactively.

New words

does> ( pfa ) - Marks the end of the compile time behaviour in a definition and the start of the run time behaviour it will exhibit. It pushes the pfa of the word defined onto the stack.

compile ( ) - Used in immediate definitions. Places the following words address inside the definition, making it possible to describe run time behaviour. Not available in gforth.

[compile] ( ) - Used in definitions to move the compile time behaviour of an immediate word to run time.

literal Compile time ( n ) Run time ( n ) - Compiles a literal value on the stack by embedding the run time code for pushing the value onto the stack and the value itself inside a definition.

[ ( ) - Stop compilation and start executing words.

] ( ) - Start compilation mode again. These words can be used in conjuction with literal to do i.e. arithmetic at compile time.