SkillAgentSearch skills...

Punyforth

Forth inspired programming language for the ESP8266

Install / Use

/learn @zeroflag/Punyforth

README

Build Status

Punyforth

<img src="screenshot/esp8266.jpg" align="left">

Punyforth is a simple, stack-based, Forth inspired programming language that primarily targets Internet of Things (IOT) devices, like the ESP8266. The ESP8266 is a low-cost Wi-Fi capable chip with a 80 MHz Xtensa LX3 32 bit CPU, TCP/IP stack, GPIO pins and 512 KiB to 4 MiB flash memory. It is widely used in IoT applications and home automation projects.

Punyforth also runs on x86 (Linux), ARM (Raspberry PI) but these are not the primary supported targets.

Design goals

  • Simple
  • Highly interactive
  • Extensible
  • Small memory footprint and resource efficiency

Quick start

The easiest way to try out Punyforth is to use a ESP8266 based development board that has USB to serial interface on board (Geekcreit/Doit, Amica, WeMos, LoLin). Connect the development board to your computer via USB. Let's assume the serial port is COM4.

$ cd arch/esp8266/bin
$ python flash.py COM4

The flash.py utility will store the Punyforth binary and modules source code on the flash memory of the esp8266.

Open a serial terminal<sup>1</sup> on port COM4 then type:

println: "Hello world!"
<img src="screenshot/helloworld.png" align="center" height="494" width="697" >

<a name="serial">1</a>: Baud rate: 115200 bps. Local echo: on, line mode: enabled. You can find some free terminal emulators here.

Note that flash.py flashes with Quad I/O speed (qio) by default. This is the fastest mode but not all devices support this. If you have trouble while flashing try adding a --flashmode dio parameter.

Now let's do some simple arithmetics.
4
dup
+
.

This should give you the following output.

(stack)
(stack 4)
(stack 4 4)
(stack 8)
(stack)
8

Congratulation, you've just doubled a number and printed out the result in the REPL.

For a detailed getting started guide see Developing and deploying Punyforth applications.

About the language

Punyforth is a simple, imperative, stack-based, concatenative programming language and interactive environment with good metaprogramming support and extensibility.

The Forth environment combines the compiler with an interactive shell (REPL), where the user can define functions called words.

Punyforth does not have local variables, instead values are kept on a stack. This stack is used only for storing data. There is a separate return stack that stores information about nested subroutine calls. Both stacks are first-class in the language.

As a consequence of the stack, Punyforth uses a form of syntax known as Reverse Polish or Postfix Notation.

If you type the following code in the REPL:


1 2 +

The interpreter pushes the number 1 then the number 2 onto the data stack. It executes the word +, which pops the two top level items off the stack, calculates their sum, and pushes the result back to the stack.

The following code calculates a * a + b * b.

2 3                  \ let's say a is 2, b is 3
dup * swap dup * + . \ prints out 13

The word dup duplicates the top level item of the stack. The word swap exchanges the two top level items of the stack.

Stack visualization:

<pre> 2 3 3 9 2 2 4 13 2 3 2 9 2 9 2 9 </pre>

dup and swap are stack shuffle words. Excessive use of words like them makes the code hard to follow, so it is advisable to use them sparingly. There are many ways to reduce the number of stack shuffles, one of them is to use quotations and combinators.

For example the above code could have been expressed the following way:

2 3 { square } bi@ +

Where square is defined as dup *.

See the chapter about quotations and combinators for more information.

Differences between Punyforth and other Forth systems

Punyforth is heavily inspired by the Forth programming language. It uses the same compilation model (outer interpreter, compiler, modes, dictionary, immediate words, etc.) as other Forth systems. Punyforth is bootstrapped from a small set of primitives written in assembly language. The compiler targets these primitives and compiles indirect-threaded code. Higher level abstractions are built on top of the primitives therefore most of the system is written in itself (in Forth).

Some of the differences

  • Punyforth is case sensitive
  • Strings are null-terminated
  • String literals ("Hello World") and character literals ($A) are supported
  • Strings can be printed out differently (print: "foobar" instead of ." foobar")
  • Parsing words are ended with a colon character by convention (including variable:, constant:, create: does>)
  • Defining a word in terms of itself results recursion by default (use the override word to alter this behaviour)
  • Curly brackets denote quotations instead of locals

Punyforth supports exception handling, multitasking, socket and GPIO APIs and comes with a UART and a TCP REPL.

Programming

During programming, the user uses the REPL to write and test small piece of codes or to extend the languge with new words (which are called subroutines or functions in other languages).

The REPL (also known as the Forth Outer/Text Interpreter) operates in 2 modes. In interpretation mode, it immediately executes the words that the user typed in. In compilation mode (when you start a new word definition), its action depends on the compilation semantic of the current word. In most cases it compiles the execution token (pointer to the word) into the word to be defined. However, if the current word is flagged as immediate, the compiler executes the word at compile time so the word can define its own compilation semantic. This is a bit similar to Lisp macros. Control structures are implemented as immediate words in Forth.

The syntax

Forth has almost no syntax. It grabs tokens separated by whitespace, looks them up in a dictionary then executes either their compilation or interpretation semantic. If the token is not found in the dictionary, it tries to convert it to a number. Eeverything in Forth is either a word or a number. Because of the postfix notation there are no precedence rules and parentheses.

 This is an example of
 valid   Forth syntax 123  *&^%$#@2

Extending the dictionary

Words are stored in a dictionary. The dictionary maps words to executable code or data structures.

You can use defining words to extend the dictionary with new definitions. The most basic defining words is the : (colon). This adds a new word to the dictionary with the behavior defined in terms of existing words. A colon definition begins with a colon and ends with a semicolon.

: square ( n -- n^2 ) dup * ;

4 square .      \ prints 16

In the above example we created a new word called square that takes a number off the stack, multiplies it with itself, then leaves the result on the stack. The ( n -- n^2 ) is the optional stack effect comment indicating the input and output parameters.

Other common defining words are variable: and constant:.

variable: var1                \ create a variable 'var1' without initializing
54 init-variable: var2        \ create a variable 'var2' and initialize it to 54
42 constant: answer           \ create a constant 'answer' with the value 42

var2 @ var1 !   \ assigns the value of var2 to var1
var1 ?          \ prints out 54
answer .        \ prints out 42

Control structures

Punyforth supports the regular Forth conditional and loop words.

Conditionals

General form of if else then.

<bool> if <consequent> else <alternative> then

For example:

: max ( a b -- max )
  2dup < if nip else drop then ;

10 100 max . \ prints 100

The else part can be omitted.

: abs ( n -- absn )
  dup 0< if -1 * then ;

-10 abs . \ prints 10

Case statement

Punyforth also supports switch-case like flow control logic as shown in the following example.

: day ( n -- )
  case
    1 of print: "Monday" endof
    2 of print: "Tuesday" endof
    3 of print: "Wednesday" endof
    4 of print: "Thursday" endof
    5 of print: "Friday" endof
    6 of print: "Saturday" endof
    7 of print: "Sunday" endof
    print: "Unknown day: " .
  endcase ;

Count-controlled loops

The limit and start before the word do defines the number of times the loop will run.

<limit> <start> do <loop-body> loop

Do loops iterate through integers by starting at start and incrementing until you reach the limit. The word i pushes the loop index onto the stack. In a nested loop, the inner loop may access the loop variable of the outer loop by using the word j.

For example:

5 0 do i . loop \ prints 01234

There is an other version of the do loop where you can define the increment (which can be negative as well).

<limit> <start> do <loop-body> <increment> +loop

For example:

10 0 do i . 2 +loop \ prints 02468

If the increment is negative then limit is inclusive.

0 8 do i . -2 +loop \ prints 864

Related Skills

View on GitHub
GitHub Stars421
CategoryDevelopment
Updated13d ago
Forks41

Languages

Forth

Security Score

85/100

Audited on Mar 18, 2026

No findings