Skip to content

loscoala/goforth

Repository files navigation

goforth

This is a compiler and byte code interpreter for a forth-like language.

Static Badge Static Badge Static Badge Static Badge

In general, Forth is, among other things, a programming language whose ideals are freedom and simplicity. Freedom requires knowledge and responsibility, and simplicity is yet more demanding. But "simple" can also mean rudimentary. This Forth attempts to be simple as possible and also fast as possible.

In this forth you can define local variables with curley brackets like in other forths and produce callable byte code subroutines. The compiler produces human readable bytecode and is amazingly fast.

Overview

goforth is the interactive compiler which produces a sequence of commands for a virtual stack machine also known as "Forth machine". The code is then interpreted and executed.

Why goforth?

  • Goforth can be used as an embeddable programming language. See topic embedding.
  • Goforth can be used as a compiler to produce C-code from forth. See topic Generate C-code.
  • Goforth can be embedded in all sorts of different documents using the template sytax. See topic templating.

Currently there is no parallel ForthVM execution via goroutines.

Installation

If you do want to get the latest version of goforth, change to any directory that is both outside of your GOPATH and outside of a module (a temp directory is fine), and run:

go install github.com/loscoala/goforth/cmd/goforth@latest

If you do want to add the latest version to your go.mod inside your project run:

go get github.com/loscoala/goforth@latest

Build and Dependencies

  • All you need is a golang compiler 1.22

  • Build the project with build.sh or go build -C cmd/goforth

  • For C code generation you need a working C compiler.

git clone https://github.com/loscoala/goforth.git
./build.sh

Usage

Execute goforth. See "Installation"

  1. You can also execute forth-scripts:
goforth --file=forthscript.fs
echo ": main 5 5 + . ;" | goforth
  1. In the Shebang you can alternatively write the following:
#!goforth --file
# code goes here...
  1. Or in the command line:
goforth -script '
: myfunc ." Hello World" ;
: main myfunc ;
'

The compiler has core.fs automatically included into the binary.

Howto start

Start ./goforth and in the REPL you can look what the dictionary contains by typing:

command description
% Shows the complete dictionary
% name Shows the definition of name in the dictionary
use filename Loads a file
$ Shows the values of the stack

Examples

Global variables

Globals are defined with the variable compiler-builtin keyword.

Lets define a variable xx. This defines an entry in the global dictionary.

forth> variable xx

Now you can set for example the value 15 to xx:

forth> 15 to xx
forth> xx .  \ prints the value of variable xx

Or lets sum all values from 0 to 100:

forth> variable vv
forth> 101 0 [ vv + to vv ] for vv .

Mandelbrot

Start the interpreter:

./cmd/goforth/goforth

Then inside the REPL type the following:

forth> use examples/mandelbrot.fs \ loads the file mandelbrot.fs and parses it
forth> mb-init                    \ compile and run mb-init
forth> true debug                 \ OPTIONAL in order to show byte code and benchmark
forth> mb                         \ compile and run mb

As a result you see a zoom in the mandelbrot fractal.

mandelbrot-video

Interactive development

You can also define new words:

forth> : myadd 2 + ;

Or words with a local context:

: fakulty
  1 { x }
  1+ 1 ?do
    x i * to x
  loop
  x
;

then run it:

forth> 5 fakulty .

which prints:

120

Generate C-Code

To translate one or more words into C code and generate a native binary file there is a compile statement.

This is how the html.fs example written in Forth can be easily translated into C and executed immediately:

forth> use examples/html.fs
forth> compile test

The word test was defined in the sample file as follows:

: test
  document
  [
    [
      [ ." charset=\"utf-8\"" ] meta
      [ ." Example Page" ] title
    ] head
    [
      [ ." Example Page" ] h1
      [ ." Hello " [ ." World!" ] b ] p
    ] body
  ] html
;

After the compile test statement, a C file main.c was generated in the lib directory and compiled with the C compiler and executed.

The result is also shown as follows:

<!doctype html>
<html lang="de-DE"><head><meta charset="utf-8"><title>Example Page</title></head><body><h1>Example Page</h1><p>Hello <b>World!</b></p></body></html>

Call external programs

The following example shows the usage of the sh word to list all the files and directories in the current directory under linux machine.

: ls
  100 [
    [ s" ls -l" ] sh
  ] alloc
;

Optional: Now you can compile the word to a native binary.

forth> compile ls

Debugging

By calling true debug you can enable the benchmark mode.

forth> true debug

Now the byte code is displayed and the execution time of the program.

forth> true debug
forth> 5 3 min .
SUB min;TDP;LSI;JIN #0;DRP;JMP #1;#0 NOP;SWP;DRP;#1 NOP;END;
MAIN;L 5;L 3;CALL min;PRI;STP;
3

execution time: 15.947µs
Number of Cmds: 13
Speed: 0.000815 cmd/ns

The actual debugger can be run like this:

forth> debug 34 21 min .

Which gives the following result:

SUB min;TDP;LSI;JIN #0;DRP;JMP #1;#0 NOP;SWP;DRP;#1 NOP;END;
MAIN;L 34;L 21;CALL min;PRI;STP;

 11 MAIN            |                           |                           |
 12 L 34            | 34                        |                           |
 13 L 21            | 34 21                     |                           |
 14 CALL min        | 34 21                     | 14                        |
  1 TDP             | 34 21 34 21               | 14                        |
  2 LSI             | 34 21 0                   | 14                        |
  3 JIN #0          | 34 21                     | 14                        |
  6 NOP #0          | 34 21                     | 14                        |
  7 SWP             | 21 34                     | 14                        |
  8 DRP             | 21                        | 14                        |
  9 NOP #1          | 21                        | 14                        |
 10 END             | 21                        |                           |
 15 PRI             |                           |                           | 21
 16 STP

As you can see on the top there is the ByteCode and below you see the program pointer, the command, the stack, the return stack and the output.

Embedding

First, you have to add goforth to go.mod:

go get github.com/loscoala/goforth@latest

Then in golang you can import goforth:

import "github.com/loscoala/goforth"

Now all you need is a ForthCompiler:

fc := goforth.NewForthCompiler()

fc.Fvm.Sysfunc = func(fvm *goforth.ForthVM, syscall int64) {
  switch syscall {
  case 999:
    value := fvm.Pop()
    fvm.Push(value + value)
    fmt.Println("This is a custom sys call in Forth")
  default:
    fmt.Println("Not implemented")
  }
}

// Parse the Core lib:
if err := fc.Parse(goforth.Core); err != nil {
  goforth.PrintError(err)
}

// Run some code:
if err := fc.Run(": main .\" Hello World!\" ;"); err != nil {
  goforth.PrintError(err)
}

// Call custom syscall (calculated 10+10 and prints it):
if err := fc.Run(": customcall 999 sys ; : main 10 customcall . ;"); err != nil {
  goforth.PrintError(err)
}

Templates

Goforth has a meta command called template <name> <filename>. When goforth parses a template file, it looks for opening and closing tags, which are <?fs and ?> which tell goforth to start and stop embedding the code between them. Parsing in this manner allows goforth to be embedded in all sorts of different documents, as everything outside of a pair of opening and closing tags is ignored by the goforth parser.

Example #1 Goforth opening and closing tags

Example file html.tfs:

<html>
  <head>
    <title><?fs title .s ?></title>
  </head>
  <body>
<?fs 10 0 do ?>
    <p><?fs i . ?></p>
<?fs loop ?>
  </body>
</html>

Now you can load the template named test:

forth> template test html.tfs
forth> variable title
forth> a" Example title" to title
forth> test

Description of the files

filename description
main.go The main function
compiler.go The forth compiler
vm.go A stack machine with additional functionality for forth
stack.go A stack of strings implementation
label.go A label generator for the bytecode
show.go Visual representation and the REPL
config.go Configuration during runtime

VM commands

The stack machine consists of a single LIFO stack and memory. Memory is adressed by an unsigned number. The stack and memory consists only of numbers.

command description
RDI Reads a value from the input. The input is implementation dependant.
PRI Prints a value from the stack as a number.
PRA Prints a value from the stack as an character.
DUP Duplicates the top stack value.
OVR Copies the second value from the top on the top.
TVR Copies the second pair onto the top pair.
TWP Swaps the top pair with the pair under it.
QDP ?dup implemented as: if the top value is zero - duplicate it - otherwise do nothing.
ROT Rotates the top three elements by pushing to stack one element "down" an taking the last element on the top.
TDP Duplicates the top pair on the stack.
DRP Drops the top element on the stack.
SWP Swaps the top pair on the stack.
#id NOP Does nothing. Used for labeling.
JMP #id Unconditional jump to label with id
JIN #id Conditional jump to label with id if the top value on the stack is zero.
ADI Simple addition of the top pair.
SBI Simple subtraction of the top pair.
DVI Simple division of the top pair.
LSI 1 on the stack if the top element is less than the second element. 0 otherwise.
GRI 1 on the stack if the top element id greater than the second element. 0 otherwise.
MLI Multiplies the first two elements on the stack.
ADF Simple float addition of the top pair.
SBF Simple float subtraction of the top pair.
MLF Multiplies the first two float elements on the stack.
DVF Simple float division of the top pair.
PRF Prints a float value from the stack as a number.
LSF 1 on the stack if the top float element is less than the second element. 0 otherwise.
GRF 1 on the stack if the top float element is greater than the second element. 0 otherwise.
OR 1 if one of the first two elements on the stack unequal to 0 else 0.
AND 1 if the first two elements on the stack unequal to 0 else 0.
NOT 1 if the first element on the stack is 0 else 0.
EQI 1 if the first two element on the stack are equal else 0.
LV Loads a value from the given address.
L number Loads a value on the stack.
LF float Loads a float on the stack.
STR Stores the second element from the stack into the memory address which is the first element from the stack.
SYS Make a syscall. The top element from the stack is used to make different syscalls.
STP Quits the execution.
SUB name Declares a subroutine. Used only for code generation. Not used by the vm
END Ends the subroutine. Used only for code generation. Not used by the vm
MAIN Declares the beginning of the main. Used only for code generation. Not used by the vm
GDEF Creates a new global variable initialized with zero
GSET Assigns the top value of the stack to a global variable
GBL Pushes the global value on top of the stack
LCTX Creates a new context of local variables
LDEF Pops the top value of the stack and copies it to the local definitions
LSET Assigns the top value of the stack to a local variable
LCL Pushes the local value on top of the stack
LCLR Clears the local definitions
CALL name Call a SUB routine.
REF name Pushes the address of a SUB on top of the stack
EXC Pops the top value from the stack and calls a SUB routine.
PCK dup = 0 pick
NRT -rot = rot rot
TR to r
FR from r
RF r fetch
TTR 2 to r
TFR 2 from r
TRF 2 r fetch
INC Increments the top value of the stack by 1