Learning Forth with Planckforth

2022-08-21

I've been studying Forth recently, spcifically looking at a project called Planckforth by Koichi Nakamura. It's an amazing project and it's a great representation of why I like Forth so much. I'm going to write my thoughts down as I analyze Planckforth in case it's useful for anybody else.

In general Forths are either direct-threaded or indirect-threaded. Planckforth uses indirect threading. Here's a good explainer for the differences: https://muforth.nimblemachines.com/threaded-code/

Forth is just a simple, clever, lazy VM. In order to make use of the underlying processor's primitive instructions like ADD, MULT, LOAD, OUT etc, Forth wraps each of those instructions in a thin structure. So if your goal is to ADD a million numbers together, Forth is always going to be slower than assembly language or a traditional compiled language like C. But that minimal structure that we added allows us to now connect these assembly instructions together at runtime into more complex subroutines written in Forth. And because of the nature of that thin wrapper structure, we can subsequently connect Forth words, assembly primitives and any data we want into even more Forth words, like clicking Lego together.

Forth has a pair of very useful words called CREATE and DOES>.

Action                                                                                                                 Input buffer                          Stack after action 
---------------------------------------------------------------------------------------------------------------------  ---------------------------           ----------------------
**Phase 0: (does>) and does> are defined:**
input the text : (does>) latest >cfa 3 cells + ! ;                                                                      : (does>) latest >cfa 3 cells + ! ;
    execute ":"
        enter compile mode                                                                                                                                   ( 1 2 3)
        build header for new word (does>)                                                                                foobar
        compile docol
    compile "latest"
    compile ">cfa"
    compile "3"
    compile cells
    compile +
    compile !
    execute ;
back to interpreter
type the command : does> align 0 [compile] literal here cell- compile (does>) [compile] ; :noname swap ! ; immediate
    execute :                                             
        enter compile mode
        build header for new word `does>`
        compile docol
    compile align
    compile 0
        at this point 0 is a word that pushes the number 0 to the stack at runtime
    execute [compile] since it's immediate
        execute '
            fetch word from stdin (pushes xt of word `literal` to stack)
        execute ,
            store xt of `literal`
        execute exit
    compile here
    compile cell-
    execute compile
        execute '
            fetch word from stdin (pushes xt of (does>) to stack)
        execute (compile)
            execute [compile]
                execute '
                    fetch word from stdin (pushes xt of ; to stack)
                execute literal
                    store L:lit and ;

                
            litera
    [ ' , ] literal ,   \ compile ,

1) indexed-array is defined:
type the command : indexed-array create cells allot does> swap cells + ;
    execute word ":"
        enter compile mode
        build header for new word indexed-array
        compile "docol"
    compile "create"                                                            cells allot does> swap cells + ;
    compile "cells"                                                             allot does> swap cells + ;
    compile "allot"                                                             does> swap cells + ;
    execute does> since it's immediate                                               swap cells + ;
        execute align
        execute 0                                    ( 0 )
        execute literal
            stores L:lit and 0                       ( )
        execute here                                 ( addr-of-Lit0+1 )
        execute cell-                                ( addr-of-Lit0 )
        execute L:lit
            pushes (does>)                           ( addr-of-Lit0 (does>) )
        execute ,                          
            stores (does>)
        execute ;
        execute :noname                              ( addr-of-Lit0 addr-of-noname )
        execute swap                                 ( addr-of-noname addr-of-Lit0 )
        execute !
            writes addr-of-noname to Lit0's argument
        e:exit
    compile word "swap"                                                              cells + ;
    compile word "cells"                                                             + ;
    compile word "+"                                                                 ;
    execute ; since it's immediate
        stores "exit"
        exit compile mode

2) indexed-array is executed, fooooo is defined      
    type the command                                                                  20 indexed-array fooooo
    execute 20                                         ( 20 )                         indexed-array fooooo
    execute indexed-array
        execute create                                 ( 20 )                         fooooo
            store new dict header
            fetch word from stdin, store it
            store docol
            store L:lit
            store here + 3cells
            store nop
            store exit
        execute cells                                   ( 20*cell )
        execute allot                                   ( )
            here += 20*cell
        execute L:lit
            pushes (ptr to anon func)                   ( ptr-to-anon-func )
        execute (does>)
            execute latest                              ( ptr-to-anon-func dict-entry-of-fooooo )
            execute >cfa                                ( ptr-to-anon-func cfa-of-fooooo )
            execute 3 cells +                           ( ptr-to-anon-func foo-nop-ptr )
            execute !
                replaces fooooo's final nop with a call to the anon func
            execute exit from end of (does>)
        execute exit from end of indexed-array
    back at interpreter
    
3) fooooo is executed
    type the command                                                                  5 fooooo
    execute 5                                           ( 5 )
    execute fooooo
        execute L:lit
            push pointer to fooooo's storage space      ( 5 fooooo-storage )
        execute anonymous-func
            execute swap                                ( fooooo-storage 5 )
            execute cells                               ( fooooo-storage 5*cells )
            execute +                                   ( fooooo-storage+5*cells )
            execute exit from anonymous-func
        execute exit from fooooo
    back at interpreter

Some good pages for learning about Forth: - http://yosefk.com/blog/my-history-with-forth-stack-machines.html - https://www.forth.com/starting-forth/9-forth-execution/ - https://www.bradrodriguez.com/papers/moving1.htm - https://www.bradrodriguez.com/papers/moving2.htm - https://github.com/TG9541/stm8ef

Washing machine application from "Forth Programmer's Handbook":

\ Port addresses
HEX
7000 CONSTANT MOTOR3 7002 CONSTANT VALVE
7004 CONSTANT FAUCETS 7006 CONSTANT DETERGENT
7008 CONSTANT TIMER 700A CONSTANT CLUTCH
7010 CONSTANT LEVEL
DECIMAL

\ Basic commands
: ON ( port -- )   -1 SWAP OUTPUT ;
: OFF ( port -- )  0 SWAP OUTPUT ;
: SECONDS ( n -- )  1000 * MS ;
: MINUTES ( n -- )  60 * SECONDS ;

\ Machine functions
: ADD ( port -- )  DUP ON  10 SECONDS  OFF ;
: ?FULL ( -- n )  LEVEL INPUT ; \ factored out from TILL-FULL in "Starting FORTH"
: TILL-FULL ( -- )  BEGIN  ?FULL  UNTIL ;
: DRAIN ( -- )  VALVE ON  3 MINUTES  VALVE OFF ;
: AGITATE ( -- )  MOTOR ON  10 MINUTES  MOTOR OFF ;
: SPIN ( -- )  CLUTCH ON  MOTOR ON  5 MINUTES
   MOTOR OFF  CLUTCH OFF ;
: FILL ( -- )  FAUCETS ON  TILL-FULL  FAUCETS OFF ;

\ Sequencing
: WASH ( -- )  FILL  DETERGENT ADD  AGITATE  DRAIN ;
: RINSE ( -- )  FILL  AGITATE  DRAIN ;
: WASHER ( -- )  WASH  SPIN  RINSE  SPIN ;

https://github.com/bfox9900/CAMEL99-ITC/blob/master/CosmicConquest/On%20Forth%20Coding%20Style.md

from Forth Dimensions May/June 1992 http://www.forth.org/fd/FD-V14N1.pdf

: attribute create 0 ,
            does>  create dup @ 1 rot +! ,
            does>  @ ;

attribute color
attribute shape

color red     color blue    color green
shape round   shape square  shape oval