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 ( 20cell ) execute allot ( ) here += 20cell 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 5cells ) execute + ( fooooo-storage+5cells ) 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