Output & Running It
Hello, World
Forth reads its source as words separated by whitespace and acts on each one immediately.
: begins the definition of a new word named GREET, and ; ends it. Inside, ." prints text up to the closing quote and CR starts a new line. Naming GREET on the next line runs it.function greet() {
console.log("Hello, World!");
}
greet();: GREET ." Hello, World!" CR ;
GREETA JavaScript semicolon ends a statement; a Forth semicolon ends a definition, and it is a word like any other. There are no statements in Forth at all — only words, run one after another.
Printing Numbers
A number in the source is pushed onto the data stack.
* multiplies the top two numbers, and . prints the top number and removes it. . prints a space after every number, which is why the second line needs no separator.const width = 12;
const height = 5;
console.log(width * height);
console.log(width, height);12 5 * . CR
12 . 5 . CRconsole.log takes any number of arguments of any type and decides how to show each. Forth has a different printing word for each kind of thing — . for a signed number, U. for an unsigned one, EMIT for a character, TYPE for a string — because nothing on the stack carries a type that could tell a single word what to do.Comments and Documentation
\ comments out the rest of a line, and ( comments out everything up to ). The parenthesized ( side -- area ) is a stack-effect comment: what the word expects on the stack to the left of --, and what it leaves to the right./**
* @param {number} side
* @returns {number}
*/
function area(side) {
return side * side; // multiply the side by itself
}
console.log(area(9));\ Squares a length.
: AREA ( side -- area ) DUP * ; \ copy the side, then multiply
9 AREA . CRA stack-effect comment is Forth's JSDoc, and like JSDoc nothing enforces it. It is written for every word, because with no parameter names in the code it is the only place a reader can learn what a word takes and gives back.
A Stack You Can See
The Stack Is an Array You Cannot Name
The JavaScript example spells out what the Forth line does. Each number is a
push; + pops two values and pushes their sum; * does the same with a product; . pops the result and prints it.const stack = [];
stack.push(2);
stack.push(3);
stack.push(stack.pop() + stack.pop());
stack.push(4);
stack.push(stack.pop() * stack.pop());
console.log(stack.pop());2 3 + 4 * . CREvery Forth program works on this one shared stack, and every word takes its inputs from it and leaves its outputs on it. That is the whole calling convention: there are no argument lists because the arguments are already there, in order, when a word starts.
Rearranging the Stack
Without names, values are rearranged instead of referred to.
DUP copies the top item, SWAP exchanges the top two and OVER copies the second item onto the top. The printing words pop from the top, so a stack of 4 9 4 prints as 4 9 4 read from the end.let [second, top] = [4, 9];
console.log(top * top); // DUP: use the top twice
[second, top] = [top, second];
console.log(top, second); // SWAP, printed from the top down
[second, top] = [4, 9];
console.log(second, top, second); // OVER, printed from the top down9 DUP * . CR \ DUP copies the top
4 9 SWAP . . CR \ SWAP exchanges the top two
4 9 OVER . . . CR \ OVER copies the second to the topDestructuring assignment is JavaScript's way of shuffling values around without a temporary variable; in Forth that is the only way anything is ever done. The Forth prints each stack from the top down, which is why
SWAP . . shows 4 9.Counting What Is on the Stack
.S prints the whole stack, bottom to top, without changing it. DEPTH pushes the number of items on the stack.const stack = [10, 20, 30];
console.log(stack.join(" "));
console.log(stack.length);10 20 30
.S CR
DEPTH . CRDEPTH prints 3 rather than 4 because it counts the stack before pushing its own answer. .S is what a Forth programmer types between lines at an interactive prompt the way a JavaScript developer inspects a variable in the console; some Forths prefix its output with the depth, as in <3> 10 20 30.Working With Pairs
ROT brings the third item to the top, so ROT + adds the two y values. 2/ halves a number. >R parks the y result on the return stack while the x values, now on top, are added and halved, and R> brings y back. The word leaves two results, x underneath y.function midpoint(x1, y1, x2, y2) {
return [(x1 + x2) / 2, (y1 + y2) / 2];
}
const [x, y] = midpoint(2, 4, 10, 20);
console.log(x, y);: MIDPOINT ( x1 y1 x2 y2 -- x y )
ROT + 2/ >R + 2/ R> ;
2 4 10 20 MIDPOINT SWAP . . CRReturning two numbers needs no array: the word simply leaves both.
2/ is an arithmetic shift, so it is exact for the even sums here; JavaScript's / 2 would have produced a fraction for an odd one, where 2/ rounds down.Words Instead Of Functions
Define Before You Use
A Forth word must be defined before any code that uses it is read. The compiler reads the source once, from top to bottom, and looks each word up in the dictionary the moment it meets it.
console.log(describe(8));
function describe(number) {
return number * 2;
}: DESCRIBE ( n -- n*2 ) 2 * ;
8 DESCRIBE . CRJavaScript hoists function declarations to the top of their scope, so the call above the definition works. Forth has no hoisting, and moving the last line of the Forth example to the top would stop with "undefined word: DESCRIBE". Forth programs are therefore written bottom-up: the smallest words first, the program's entry word last.
Small Words
SUM-OF-SQUARES squares the top number, SWAPs so the other is on top, squares that, and adds. A name may contain hyphens — any character but a space — so no camel case is needed.const square = (number) => number * number;
const sumOfSquares = (first, second) => square(first) + square(second);
console.log(sumOfSquares(3, 4));: SQUARE ( n -- n*n ) DUP * ;
: SUM-OF-SQUARES ( a b -- n ) SQUARE SWAP SQUARE + ;
3 4 SUM-OF-SQUARES . CROne-line arrow functions are about as close as JavaScript gets to typical Forth, where most words fit on a line and a word longer than a few lines is considered a design problem.
Recursion
A word cannot call itself by name while it is still being defined, because the name is not added to the dictionary until the final
;. RECURSE is the word for "call the word being defined". Indices below 2 are returned as they are by leaving them on the stack and EXITing.function fibonacci(index) {
return index < 2 ? index : fibonacci(index - 1) + fibonacci(index - 2);
}
console.log(fibonacci(20));: FIBONACCI ( index -- n )
DUP 2 < IF EXIT THEN
DUP 1- RECURSE SWAP 2 - RECURSE + ;
20 FIBONACCI . CRThe late visibility of the name is what allows a new word to be built on an old word of the same name, something JavaScript's single binding per scope cannot express. Both versions make the same 21,891 calls; the Forth one is fast enough that the difference is invisible here.
Changing a Word Later
Defining
FORMAT a second time adds a new word that hides the old one from any code compiled afterward. REPORT was compiled against the first FORMAT, and it keeps using it.let format = (value) => "value: " + value;
const report = (value) => console.log(format(value));
report(7);
format = (value) => "[" + value + "]";
report(7);: FORMAT ( n -- ) ." value: " . ;
: REPORT ( n -- ) FORMAT CR ;
7 REPORT
: FORMAT ( n -- ) ." [" 0 <# #S #> TYPE ." ]" ;
7 REPORTReassigning a JavaScript variable changes what every closure that reads it sees, so the second
report prints [7]. Forth resolved FORMAT once, when REPORT was compiled — so the Forth output line is unchanged, and the new FORMAT is used only by code written after it. A deferred word, shown further down, is how Forth opts into JavaScript's behavior.Numbers Are Integers
No Floating Point
Every value in core Forth is an integer, so
/ is integer division and there is no way to write 0.1 at all. Programs count in the smallest unit they need — tenths, cents, millimeters — so the first line adds one tenth and two tenths as the whole numbers 1 and 2.console.log(0.1 + 0.2);
console.log(Math.trunc(7 / 2));1 2 + . CR \ tenths, counted as whole numbers
7 2 / . CREvery JavaScript number is a 64-bit binary floating-point value, which is why
0.1 + 0.2 is not exactly 0.3. Forth sidesteps that by not having fractions, and 7 2 / gives 3 where JavaScript's 7 / 2 gives 3.5 — the same as Math.trunc(7 / 2). Most Forths offer a separate floating-point word set with its own stack, but it is optional and this one does not have it.The 32-Bit Integers JavaScript Already Has
A Forth number is a fixed-size cell, 32 bits here, and adding past the largest one wraps around to the most negative.
U. prints a cell as unsigned, so −1 — every bit set — prints as 4294967295.const largest = 2147483647;
console.log((largest + 1) | 0);
console.log(largest + 1);
console.log(-1 >>> 0);2147483647 1+ . CR
-1 U. CRJavaScript's bitwise operators convert their operands to 32-bit integers, so
| 0 and >>> 0 are windows onto exactly the arithmetic Forth does all the time. The plain largest + 1 line is what Forth cannot do: a floating-point number simply keeps growing. On a 64-bit Forth the wrap happens at a much larger number.Shifts
RSHIFT is a logical shift that fills with zeros, like >>>, and LSHIFT is <<. The arithmetic shift that keeps the sign, >>, has only a one-bit version in core Forth: 2/.console.log(-16 >> 2);
console.log(-16 >>> 28);
console.log(3 << 4);-16 2/ 2/ . CR
-16 28 RSHIFT . CR
3 4 LSHIFT . CRThe three lines print the same numbers in both languages because both are doing 32-bit integer shifts. A 64-bit Forth agrees on the first and last lines and not the middle one: shifting a negative number right with zeros coming in depends on how many bits there were to begin with. The difference is only in naming: Forth writes shift counts after the value, like every other operand.
Remainders and Negative Numbers
Forth names both kinds of integer division.
SM/REM truncates toward zero, leaving a remainder with the dividend's sign; FM/MOD rounds toward negative infinity, leaving a remainder with the divisor's sign. Both divide a double-width number — S>D makes one — and leave the remainder under the quotient, so . . prints the quotient first.console.log(Math.trunc(-17 / 5), -17 % 5);
console.log(Math.floor(-17 / 5), ((-17 % 5) + 5) % 5);: SYMMETRIC ( n d -- ) >R S>D R> SM/REM . . CR ;
: FLOORED ( n d -- ) >R S>D R> FM/MOD . . CR ;
-17 5 SYMMETRIC
-17 5 FLOOREDJavaScript's
% is the symmetric kind, and getting the floored one takes the ((n % d) + d) % d idiom that every JavaScript developer who has wrapped an index around an array has written. Plain / and MOD in Forth may be either kind, depending on the system, so portable code that divides negative numbers names the one it wants.Printing in Another Base
BASE is a variable holding the radix used both for printing and for reading numbers from the source. HEX and DECIMAL set it to 16 and 10, and OCTAL here stores 8. On the last line, 777 is read in octal and printed in decimal.const permissions = 493;
console.log(permissions.toString(8));
console.log((255).toString(16).toUpperCase());
console.log(parseInt("777", 8));: OCTAL ( -- ) 8 BASE ! ;
493 OCTAL . DECIMAL CR
255 HEX . DECIMAL CR
OCTAL 777 DECIMAL . CRtoString(8) and parseInt(…, 8) each take the radix as an argument and leave nothing changed. Forth's radix is one global setting that affects the rest of the program, including every number literal after it — so every line puts DECIMAL back before moving on.Truthiness
Only Zero Is False
IF takes a number from the stack and treats zero as false and anything else as true.for (const value of [0, 7, -1, "", null, NaN]) {
console.log(String(value) || '""', value ? "truthy" : "falsy");
}: TRUTHY? ( n -- ) DUP . IF ." truthy" ELSE ." falsy" THEN CR ;
0 TRUTHY?
7 TRUTHY?
-1 TRUTHY?JavaScript has six falsy values beyond
false itself — 0, "", null, undefined, NaN and 0n. Forth has one, because it has only numbers: there is no empty string, no null and no NaN to be falsy. That removes a whole category of surprise, and replaces it with the need to remember what a number on the stack was supposed to mean.Comparisons Leave -1 or 0
A comparison leaves a flag: −1 for true and 0 for false. −1 has every bit set, so
TRUE is the same number as 0 INVERT.console.log(Number(5 > 3), Number(5 < 3));
console.log(~0);5 3 > . 5 3 < . CR
TRUE . CRNumber(true) is 1 in JavaScript; the equivalent Forth flag is −1, the same as ~0. The choice means AND, OR and INVERT work as logical operators on flags and as bitwise operators on everything else, with one set of words for both.No Short-Circuit Evaluation
AND is an ordinary word that takes two numbers, so both of them have to be on the stack before it runs — which means both conditions have already been computed.function checked(label, result) {
console.log("checking", label);
return result;
}
if (checked("first", false) && checked("second", true)) {
console.log("both");
}: FIRST-CHECK ( -- flag ) ." checking first" CR FALSE ;
: SECOND-CHECK ( -- flag ) ." checking second" CR TRUE ;
: DEMO
FIRST-CHECK SECOND-CHECK AND IF ." both" CR THEN ;
DEMOJavaScript's
&& skips the second operand when the first is falsy, which is why it never prints checking second. Forth runs both checks and prints both lines. When skipping matters in Forth, the second test goes inside the first test's IF.Control Flow
A Conditional That Produces a Value
IF … ELSE … THEN is not an expression, but each branch can leave a value on the stack, which comes to the same thing. The negative case leaves −1 and exits; otherwise 0> chooses between 1 and 0.const sign = (number) => (number < 0 ? -1 : number > 0 ? 1 : 0);
console.log(sign(-42), sign(0), sign(17));: SIGNUM ( n -- -1|0|1 )
DUP 0< IF DROP -1 EXIT THEN
0> IF 1 ELSE 0 THEN ;
-42 SIGNUM . 0 SIGNUM . 17 SIGNUM . CRA nested ternary is a common JavaScript way to express a three-way choice in one expression. Forth has no expressions to nest, so the same shape becomes a sequence of early exits, each leaving its answer behind.
Choosing Among Several Values
Core Forth has no
switch. Where the choices share an answer, as here, a word that computes the condition is often clearer anyway: WEEKEND? combines two equality tests with OR.function dayType(day) {
switch (day) {
case 0:
case 6:
return "weekend";
default:
return "weekday";
}
}
console.log(dayType(0), dayType(3), dayType(6));: WEEKEND? ( day -- flag ) DUP 0= SWAP 6 = OR ;
: .DAY-TYPE ( day -- ) WEEKEND? IF ." weekend " ELSE ." weekday " THEN ;
0 .DAY-TYPE 3 .DAY-TYPE 6 .DAY-TYPE CRMany Forths add
CASE … OF … ENDOF … ENDCASE for matching exact values, and it has no fall-through to forget a break for — but it is an optional part of the standard. Naming the condition, as WEEKEND? does, is the more common Forth habit.Counting Loops
6 1 DO … LOOP counts from 1 up to, but not including, 6; the limit comes first and the starting value second. I pushes the current index.let line = "";
for (let index = 1; index <= 5; index++) {
line += index * index + " ";
}
console.log(line.trim());: SQUARES 6 1 DO I DUP * . LOOP CR ;
SQUARESThe JavaScript loop builds a string and prints it once; the Forth loop prints each number as it goes, so it needs no string at all. The loop body and its counter live between
DO and LOOP, and the counter is not a variable anyone can reassign.Loops That Test at the End
BEGIN … UNTIL runs its body, then takes a flag from the stack and repeats until the flag is true — a do … while with the condition inverted. The counter sits under the number, so SWAP 1+ SWAP reaches past the number to increment it.let number = 1234;
let digits = 0;
do {
number = Math.trunc(number / 10);
digits++;
} while (number !== 0);
console.log(digits);: DIGITS ( n -- count )
0 SWAP
BEGIN 10 / SWAP 1+ SWAP DUP 0= UNTIL
DROP ;
1234 DIGITS . CRTesting at the end is what makes 0 count as one digit, in both versions. The inverted sense is the easy thing to get wrong:
UNTIL means "stop when true", so the test is DUP 0=, not DUP 0<>.Leaving a Loop Early
READINGS is an array built with ,, which stores each number in the next free memory cell. Inside the loop, READINGS I CELLS + @ fetches the element at index I. LEAVE ends the loop at once, like break.const readings = [3, 8, 12, 5, 20];
for (const reading of readings) {
if (reading > 10) {
console.log("first high reading:", reading);
break;
}
}CREATE READINGS 3 , 8 , 12 , 5 , 20 ,
: FIRST-HIGH ( -- )
5 0 DO
READINGS I CELLS + @
DUP 10 > IF ." first high reading: " . CR LEAVE THEN
DROP
LOOP ;
FIRST-HIGHA value that is not printed has to be dropped, so the loop body
DROPs every reading that is not high. Forgetting that DROP leaves a growing pile of numbers on the stack — a bug with no JavaScript counterpart, since an unused expression value simply vanishes.Variables Without Closures
A Variable Is an Address
VARIABLE VISITS reserves a cell and makes VISITS push its address. ! stores a value at an address, @ fetches the value from one, and +! adds to what is stored.let visits = 0;
visits = visits + 1;
visits += 1;
console.log(visits);VARIABLE VISITS
0 VISITS !
VISITS @ 1+ VISITS !
1 VISITS +!
VISITS @ . CREvery read and write of a Forth variable is explicit:
visits = visits + 1 becomes fetch, add, store. That is also why a variable is rare in idiomatic Forth — a value that is only needed for a moment stays on the stack, and a variable is kept for state that has to outlive the word that set it.const, and a Value You Can Reassign
CONSTANT makes a word that pushes a fixed number. VALUE makes a word that pushes a number which TO can change later. Neither needs @.const MAXIMUM_USERS = 50;
let threshold = 10;
console.log(MAXIMUM_USERS, threshold);
threshold = 25;
console.log(MAXIMUM_USERS, threshold);50 CONSTANT MAXIMUM-USERS
10 VALUE THRESHOLD
MAXIMUM-USERS . THRESHOLD . CR
25 TO THRESHOLD
MAXIMUM-USERS . THRESHOLD . CRA
VALUE reads like a let and a CONSTANT like a const, which makes them the usual choice for settings. The difference from a VARIABLE is only what the name pushes: the number itself, rather than where it lives.No Closures
A Forth word cannot capture a value from the place where it was written, because the only place values live is the shared stack, which will look completely different by the time the word runs. What stands in for a closure is a defining word:
CREATE makes a new word with its own memory, and DOES> gives it behavior. Here the memory goes unused and DROP discards its address.function makeGreeter(greeting) {
return (name) => greeting + ", " + name;
}
const hello = makeGreeter("Hello");
console.log(hello("Grace"));: GREETER ( "name" -- )
CREATE ( nothing to store yet )
DOES> DROP ." Hello, " ;
GREETER HELLO
: GRACE ." Grace" ;
HELLO GRACE CRThis is the minimum of the pattern, kept small so the shape is visible; the next sections store real data in that memory, which is where a defining word earns its keep. The JavaScript closure keeps
greeting alive for as long as hello exists; a Forth program keeps data alive by writing it into memory, and nothing is ever collected.Memory Is a Typed Array You Share
An Int32Array of Your Own
CREATE names the next free address in memory and ALLOT reserves bytes there. 7 CELLS is the number of bytes seven cells take, so this reserves the same 28 bytes an Int32Array(7) does. TEMPERATURE converts an index into the address of that element.const temperatures = new Int32Array(7);
temperatures[0] = 18;
temperatures[6] = 24;
console.log(temperatures[0], temperatures[6]);CREATE TEMPERATURES 7 CELLS ALLOT
: TEMPERATURE ( index -- address ) CELLS TEMPERATURES + ;
18 0 TEMPERATURE !
24 6 TEMPERATURE !
0 TEMPERATURE @ . 6 TEMPERATURE @ . CRAn
Int32Array is the closest thing JavaScript has to Forth memory: fixed-size integers laid end to end in a block of bytes. What Forth leaves out is everything the typed array adds — its length, and the bounds check that ignores a write past the end. Writing to index 7 here would overwrite whatever comes next in memory.Two Views of the Same Bytes
! writes a whole cell and C@ reads a single byte, and they can be pointed at the same address. 258 is 256 + 2, so as bytes it is 2 and 1 — and which of those comes first in memory depends on the machine.const buffer = new ArrayBuffer(4);
new Int32Array(buffer)[0] = 258;
const bytes = new Uint8Array(buffer);
console.log(bytes[0], bytes[1], bytes[2], bytes[3]);CREATE SLOT 1 CELLS ALLOT
258 SLOT !
SLOT C@ . SLOT 1+ C@ . SLOT 2 + C@ . SLOT 3 + C@ . CRBoth columns print
2 1 0 0: the least significant byte first, which is little-endian order. A JavaScript Uint8Array over an Int32Array's buffer and a Forth C@ on a cell's address are the same trick, and both inherit the byte order of the machine underneath. DataView exists in JavaScript precisely to choose the order explicitly; Forth leaves that to the program.Filling and Copying Memory
FILL takes an address, a count and a byte and stores that byte into every position. C, stores a single byte, the way , stores a cell. MOVE takes a source address, a destination address and a count. TYPE prints bytes as characters.const row = new Uint8Array(8);
row.fill(46);
row.set([88, 88, 88], 2);
console.log(String.fromCharCode(...row));CREATE ROW 8 ALLOT
CREATE MARKS 88 C, 88 C, 88 C,
: DEMO
ROW 8 [CHAR] . FILL
MARKS ROW 2 + 3 MOVE
ROW 8 TYPE CR ;
DEMOfill and set on a typed array are the same two operations, and String.fromCharCode turns the bytes back into text the way TYPE does. Code 46 is the period and 88 is X.An Object Laid Out in Memory
A record is two cells in a row, and each field is a word that turns the record's address into the field's address.
>ID does nothing, since the id comes first; >BALANCE skips one cell. A leading > is the customary prefix for "address of".const account = { id: 1017, balance: 250 };
account.balance += 75;
console.log(account.id, account.balance);: >ID ( account -- address ) ;
: >BALANCE ( account -- address ) CELL+ ;
CREATE ACCOUNT 1017 , 250 ,
75 ACCOUNT >BALANCE +!
ACCOUNT >ID @ . ACCOUNT >BALANCE @ . CRA JavaScript object looks its properties up by name at run time and can gain new ones whenever it likes. A Forth record has its fields decided in advance as fixed offsets, which is exactly how a C struct works and why reading a field costs one addition.
Strings Are Bytes
Length Counts Bytes
S" leaves two numbers: the address of the text and its length in bytes. NIP drops the address and keeps the length.const word = "héllo";
console.log(word.length);
console.log(new TextEncoder().encode(word).length);: WORD-LENGTH ( -- ) S" héllo" NIP . CR ;
WORD-LENGTHA JavaScript
length counts UTF-16 code units, so "héllo" is 5. Forth counts bytes, and "é" is two bytes in UTF-8, so the Forth answer is 6 — the same number TextEncoder reports. Any Forth word that walks a string byte by byte will see "é" as two characters.Walking Through a String
A string is a starting address and a count, so the character at index
I is one byte fetched from address + I. The address stays underneath throughout, which is why each step begins with DUP and the loop ends with DROP.const text = "Forth";
let codes = "";
for (let index = 0; index < text.length; index++) {
codes += text.charCodeAt(index) + " ";
}
console.log(codes.trim());: .CODES ( address length -- )
0 DO DUP I + C@ . LOOP DROP CR ;
: DEMO S" Forth" .CODES ;
DEMOcharCodeAt and C@ answer the same question for plain ASCII text. The address-and-count pair is how every Forth word receives text, so this loop shape appears in nearly every Forth string routine.Numbers Into Text
<# starts converting a number to text, each # produces one digit (a zero once the number runs out), and #> leaves the finished text as an address and length for TYPE. Three #s make three digits. The 0 in front widens the number to double width, which is what these words work on.const count = 7;
const total = 120;
console.log("item " + String(count).padStart(3, "0") + " of " + total);: .PADDED ( n -- ) 0 <# # # # #> TYPE ;
: .ITEM ( count total -- )
SWAP ." item " .PADDED SPACE ." of " . CR ;
7 120 .ITEMpadStart(3, "0") describes the result; pictured numeric output describes the steps, digit by digit from the right. TYPE prints no trailing space, unlike ., which is why SPACE is needed after the padded number and not after the total.Text Into a Number
>NUMBER converts leading digits and stops at the first character that is not one, leaving the number (in double width) and whatever text is left over. PARSE-LEADING keeps the low half of the number and how many characters were left unconverted, and the demonstration prints them in that order.console.log(parseInt("512px", 10));
console.log(parseInt("px", 10));: PARSE-LEADING ( address length -- n unconverted )
0 0 2SWAP >NUMBER NIP NIP ;
: DEMO
S" 512px" PARSE-LEADING SWAP . . CR
S" px" PARSE-LEADING SWAP . . CR ;
DEMOparseInt("512px") quietly ignores the "px"; >NUMBER tells you exactly how much it ignored — 2 characters — and leaves the decision to the caller. For "px" there are no digits at all: JavaScript returns NaN, while Forth leaves 0 with two characters unconverted, and only that count reveals the failure.Escape Sequences
S" takes its text literally and ends at the first quote. S\" reads escape sequences — \t for a tab, \" for a quote, \n for a newline — much like a JavaScript string literal.console.log("Name:\t\"Ada\"");: DEMO S\" Name:\t\"Ada\"" TYPE CR ;
DEMOHaving two words for the two behaviors is typical of Forth: rather than one literal syntax with rules, there is a small word for each job.
S\" was added in the 2012 standard; older Forths have only S".Callbacks Are Execution Tokens
Passing a Function
' TICK pushes an execution token for TICK — a number that identifies the word — instead of running it. EXECUTE runs the word whose token is on the stack. REPEAT-TIMES keeps a copy of the token and executes it on each pass.function repeat(times, action) {
for (let count = 0; count < times; count++) action();
}
repeat(3, () => console.log("tick"));: TICK ." tick" CR ;
: REPEAT-TIMES ( times xt -- )
SWAP 0 ?DO DUP EXECUTE LOOP DROP ;
3 ' TICK REPEAT-TIMESAn execution token is a callback with no type and no closure: just a number that can be passed, stored and executed. The name avoids
REPEAT, which is already the Forth word that closes a BEGIN … WHILE loop.forEach Over Memory
FOR-EACH loops with the index running over addresses rather than positions: from the start of the array to its end, stepping one cell at a time with 1 CELLS +LOOP, so I @ is the current element. The execution token waits on the data stack underneath, and OVER copies it for each call. */ multiplies and then divides.const prices = [120, 45, 300];
const withTax = (price) => console.log(price + Math.trunc(price * 8 / 100));
prices.forEach(withTax);CREATE PRICES 120 , 45 , 300 ,
: FOR-EACH ( address count xt -- )
ROT ROT CELLS OVER + SWAP \ xt end start
?DO I @ OVER EXECUTE 1 CELLS +LOOP
DROP ;
: WITH-TAX ( price -- ) DUP 8 100 */ + . CR ;
PRICES 3 ' WITH-TAX FOR-EACH🚨 The obvious alternative — parking the token on the return stack with
>R and reading it inside the loop with R@ — crashes, because DO keeps its own counters on that same return stack, so R@ reads a loop counter and EXECUTE jumps to it. Looping over addresses instead of indices is a classic Forth idiom, and it is how forEach would be written if JavaScript arrays were bare memory. The word is only a few lines, because the language has nothing to hide: an array is an address, an element is @, a callback is EXECUTE.Anonymous Words
:NONAME compiles a definition with no name and leaves its execution token on the stack. Both words are compiled first; then CREATE HANDLERS starts the array and SWAP , , stores the two tokens in order. HANDLE fetches a token by index and executes it.const handlers = [
() => console.log("saved"),
() => console.log("deleted"),
];
handlers[1]();
handlers[0]();:NONAME ." saved" CR ;
:NONAME ." deleted" CR ;
CREATE HANDLERS SWAP , ,
: HANDLE ( index -- ) CELLS HANDLERS + @ EXECUTE ;
1 HANDLE
0 HANDLEAn array of arrow functions and an array of execution tokens are the same data structure. 🚨 The order matters. Compiled code goes into the same memory that
, writes to, so a :NONAME placed between CREATE HANDLERS and its , would put the word's code where the array's first element belongs, and HANDLE would execute garbage.Swapping Behavior at Run Time
DEFER LOGGER creates a word whose action is looked up each time it runs, and IS sets that action to an execution token. So changing LOGGER changes what SAVE-FILE does, even though SAVE-FILE was compiled first.let logger = (message) => console.log("LOG " + message);
const saveFile = () => logger("file saved");
saveFile();
logger = (message) => console.log("[quiet] " + message);
saveFile();DEFER LOGGER
: SAVE-FILE ( -- ) S" file saved" LOGGER ;
:NONAME ( address length -- ) ." LOG " TYPE CR ; IS LOGGER
SAVE-FILE
:NONAME ( address length -- ) ." [quiet] " TYPE CR ; IS LOGGER
SAVE-FILEThis is the behavior a JavaScript
let gives every function reference for free, and that an ordinary Forth redefinition does not. Forth makes it an explicit choice for the few places it is wanted, which is also why a deferred word costs one extra lookup and a plain word does not.Objects and Syntax You Build
A Constructor and a Method
RECTANGLE is a defining word. Running 3 7 RECTANGLE DOOR creates a new word DOOR and stores its height and width in its body with ,. The code after DOES> is what every rectangle does when named: it receives the address of its own body, fetches both numbers and multiplies.class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
const door = new Rectangle(3, 7);
const window = new Rectangle(4, 4);
console.log(door.area(), window.area());: RECTANGLE ( width height "name" -- )
CREATE , ,
DOES> ( -- area ) DUP @ SWAP CELL+ @ * ;
3 7 RECTANGLE DOOR
4 4 RECTANGLE PANE
DOOR . PANE . CRThe "constructor" is the part before
DOES> and the one "method" is the part after, shared by every word it creates, much as area is shared through the prototype. What a Forth object cannot have is a second method — each created word does exactly one thing — so larger designs pass the object's address to ordinary words instead.A Guard Clause Keyword
?EXIT is marked IMMEDIATE, so it runs while WITHDRAW is being compiled instead of being compiled into it. When it runs, the three POSTPONEs write IF EXIT THEN into WITHDRAW: a new keyword that exits when the flag is true.function withdraw(balance, amount) {
if (amount > balance) return;
console.log("new balance", balance - amount);
}
withdraw(100, 30);
withdraw(100, 500);: ?EXIT POSTPONE IF POSTPONE EXIT POSTPONE THEN ; IMMEDIATE
: WITHDRAW ( balance amount -- )
2DUP < ?EXIT
." new balance " - . CR ;
100 30 WITHDRAW
100 500 WITHDRAWThe second call should print nothing, and in the Forth it leaves the two numbers behind unused, since the guard exits before
- consumes them. JavaScript can only add syntax like this with a build step such as a Babel plugin. In Forth the compiler is a set of words the program can add to, and IF and EXIT are themselves defined exactly this way.Building Code From Text
EVALUATE takes the address and length of some text and runs it exactly as though it had appeared in the program. Here the text is first copied into a buffer, to show that it can come from anywhere, not only from a literal.const operator = "*";
const compute = new Function("return 6 " + operator + " 7;");
console.log(compute());CREATE SOURCE-TEXT 32 ALLOT
: DEMO
S" 6 7 * . CR" DUP >R SOURCE-TEXT SWAP MOVE
SOURCE-TEXT R> EVALUATE ;
DEMOnew Function compiles its text into an isolated function. EVALUATE has no isolation at all: the text shares the program's stack and dictionary, so it can define new words or leave results that the rest of the program then uses.Errors
A Missing Word Fails Before Anything Runs
A Forth word is compiled when its definition is read, and compiling means looking up every word inside it. A word that is not in the dictionary stops the program right there, even though
NEVER-CALLED is never called.function neverCalled() {
launchRocket();
}
console.log("defined without complaint");.( about to define NEVER-CALLED) CR
: NEVER-CALLED LAUNCH-ROCKET ;
.( never printed) CRJavaScript compiles
neverCalled without checking what launchRocket refers to, and would throw a ReferenceError only if the function ran. Forth's single pass and eager lookup catch the misspelling at once, which is the other side of having to define every word before using it. The failure here is the expected one.Throwing With a Message
ABORT" takes a flag and, when it is true, prints its message, clears the stacks and abandons everything back to the outermost interpreter. When it is false, nothing happens.function setVolume(level) {
if (level > 11) throw new RangeError("volume too high");
console.log("volume", level);
}
try {
setVolume(7);
setVolume(15);
setVolume(3);
} catch (error) {
console.log(error.message);
}: SET-VOLUME ( level -- )
DUP 11 > ABORT" volume too high"
." volume " . CR ;
7 SET-VOLUME
15 SET-VOLUME
3 SET-VOLUMEThe effect is a
throw caught only by the outermost try, as in the JavaScript example — which is why setVolume(3) and 3 SET-VOLUME both never run. The failure shown is the expected one.Returning Success or Failure
FIND-INDEX leaves an index and TRUE when it finds the value, and only FALSE when it does not; the | in the stack comment separates the two outcomes. UNLOOP clears the loop's counters before EXIT leaves from inside the loop.function findIndex(values, wanted) {
const index = values.indexOf(wanted);
return index === -1 ? null : index;
}
const values = [4, 8, 15, 16];
for (const wanted of [15, 23]) {
const index = findIndex(values, wanted);
console.log(index === null ? "not found" : "found at " + index);
}CREATE VALUES 4 , 8 , 15 , 16 ,
: FIND-INDEX ( wanted -- index true | false )
4 0 DO
VALUES I CELLS + @ OVER = IF DROP I TRUE UNLOOP EXIT THEN
LOOP DROP FALSE ;
: SHOW ( wanted -- )
FIND-INDEX IF ." found at " . ELSE ." not found" THEN CR ;
15 SHOW
23 SHOWJavaScript chooses a sentinel —
-1 or null — that cannot be a real answer. Forth puts a separate flag on top instead, so any number can be a real answer, and the caller must test the flag before trusting what is underneath.try and catch
CATCH runs a word from its execution token and leaves 0 if it finished, or the number passed to THROW if it did not. On a throw, the data stack is put back to its depth before the call. TRY-AGE keeps an extra copy of the text with 2DUP so it still has something to print after a failure.function parseAge(text) {
const age = Number(text);
if (!Number.isInteger(age)) throw new Error("not a number");
return age;
}
for (const text of ["42", "forty"]) {
try {
console.log("age", parseAge(text));
} catch (error) {
console.log("could not parse", text);
}
}1 CONSTANT NOT-A-NUMBER
: PARSE-AGE ( address length -- age )
0 0 2SWAP >NUMBER NIP IF NOT-A-NUMBER THROW THEN DROP ;
: TRY-AGE ( address length -- )
2DUP ['] PARSE-AGE CATCH
IF 2DROP ." could not parse " TYPE CR
ELSE ." age " . 2DROP CR THEN ;
: DEMO S" 42" TRY-AGE S" forty" TRY-AGE ;
DEMOAn error in Forth is a number rather than an object, chosen by the program — positive numbers are free for any use, and −1 through −4095 are reserved.
CATCH and THROW belong to the optional exception word set, which is why this page's Forth can only show this example.Turtle Graphics, Both Columns
A Turtle, and a Polygon Word
A turtle has a position and a heading.
FORWARD moves it and draws a line; RIGHT turns it clockwise. A polygon with n sides turns 360 ÷ n degrees at each corner. In the Forth column the turtle words already exist, so POLYGON is the whole program; 2 PICK copies the side count from under the length and loop counter.const canvas = document.createElement("canvas");
canvas.width = 600;
canvas.height = 300;
canvas.style.width = "100%";
document.body.style.margin = "0";
document.body.appendChild(canvas);
const pen = canvas.getContext("2d");
// A turtle: a position, a heading, and two moves.
let x = 180, y = 200, heading = 0;
pen.beginPath();
pen.moveTo(x, y);
const forward = (distance) => {
x += distance * Math.sin(heading * Math.PI / 180);
y -= distance * Math.cos(heading * Math.PI / 180);
pen.lineTo(x, y);
};
const right = (degrees) => { heading += degrees; };
const polygon = (sides, length) => {
for (let side = 0; side < sides; side++) {
forward(length);
right(360 / sides);
}
};
polygon(3, 100);
polygon(5, 100);
polygon(8, 100);
pen.stroke();: POLYGON ( sides length -- )
OVER 0 DO
DUP FORWARD
360 2 PICK / RIGHT
LOOP 2DROP ;
3 100 POLYGON
5 100 POLYGON
8 100 POLYGONMost of the JavaScript is the turtle: a canvas, a position, and the trigonometry that turns "forward" into coordinates. Forth's approach to every problem is to build words like
FORWARD first and then write the program in them, and here the turtle vocabulary is that first layer, handed to you. Both drawings share one starting corner, so the three shapes fan out from the same point.A Square Spiral
Each step is a little longer than the last, and each turn is 91 degrees instead of 90, so the square slowly twists as it grows.
4 +LOOP steps the loop index by 4, and the index itself is the length of the next line. HIDETURTLE hides the arrow that marks the turtle.const canvas = document.createElement("canvas");
canvas.width = 600;
canvas.height = 300;
canvas.style.width = "100%";
document.body.style.margin = "0";
document.body.appendChild(canvas);
const pen = canvas.getContext("2d");
let x = 300, y = 150, heading = 0;
pen.beginPath();
pen.moveTo(x, y);
const forward = (distance) => {
x += distance * Math.sin(heading * Math.PI / 180);
y -= distance * Math.cos(heading * Math.PI / 180);
pen.lineTo(x, y);
};
const right = (degrees) => { heading += degrees; };
for (let length = 4; length < 200; length += 4) {
forward(length);
right(91);
}
pen.strokeStyle = "#1565c0";
pen.stroke();HEX 1565C0 DECIMAL SETPENCOLOR
HIDETURTLE
: SPIRAL ( -- )
200 4 DO I FORWARD 91 RIGHT 4 +LOOP ;
SPIRALThe same numbers in both columns give the same picture — only the size differs, because the Forth drawing is scaled to fit its pane while the JavaScript canvas is drawn at a fixed size. Changing 91 to 89 twists the spiral the other way, and 120 turns it into a triangle.
A Recursive Snowflake
A Koch curve replaces a straight line with four lines a third as long, with a spike in the middle, and then does the same to each of those. At depth 0 it is just
FORWARD. Otherwise KOCH computes the shorter length and lower depth once, then 2DUPs the pair before each of the first three recursive calls, so the fourth call can consume the original.const canvas = document.createElement("canvas");
canvas.width = 600;
canvas.height = 300;
canvas.style.width = "100%";
document.body.style.margin = "0";
document.body.appendChild(canvas);
const pen = canvas.getContext("2d");
let x = 190, y = 90, heading = 90;
pen.beginPath();
pen.moveTo(x, y);
const forward = (distance) => {
x += distance * Math.sin(heading * Math.PI / 180);
y -= distance * Math.cos(heading * Math.PI / 180);
pen.lineTo(x, y);
};
const right = (degrees) => { heading += degrees; };
const left = (degrees) => { heading -= degrees; };
const koch = (length, depth) => {
if (depth === 0) return forward(length);
koch(length / 3, depth - 1);
left(60);
koch(length / 3, depth - 1);
right(120);
koch(length / 3, depth - 1);
left(60);
koch(length / 3, depth - 1);
};
for (let side = 0; side < 3; side++) {
koch(220, 3);
right(120);
}
pen.stroke();: KOCH ( length depth -- )
DUP 0= IF DROP FORWARD EXIT THEN
1- SWAP 3 / SWAP
2DUP RECURSE 60 LEFT
2DUP RECURSE 120 RIGHT
2DUP RECURSE 60 LEFT
RECURSE ;
: SNOWFLAKE ( -- )
90 SETHEADING
3 0 DO 243 3 KOCH 120 RIGHT LOOP ;
HIDETURTLE
SNOWFLAKEThree curves joined by 120-degree turns make the snowflake. The Forth side divides a length of 243 by 3 three times and gets exactly 9, while JavaScript's 220 becomes a fraction without complaint — integer-only arithmetic is why the Forth example chose a power of three.
Gotchas For JavaScript Developers
A Word Needs Space Around It
." is a word, so it must be separated from the text by a space. Written ."tight", the whole thing is read as a single unknown word.console.log("tight");
console.log( "loose" );: LOOSE ." loose" CR ;
: TIGHT ."tight" CR ;
LOOSEJavaScript's tokenizer knows that
( and " are punctuation, so spacing never changes meaning. Forth's tokenizer knows only spaces — which is also what lets 2DUP, ?DUP and .S be names. The failure here is the expected one: TIGHT is never defined, and LOOSE never runs.HEX Changes Every Number After It
HEX is not a prefix for one literal, like 0x; it changes how every following number is read, until DECIMAL changes it back. On the first line, 10 is read in hexadecimal and becomes sixteen.const color = 0xff;
const count = 10;
console.log(color, count);: .BOTH ( a b -- ) SWAP . . CR ;
HEX FF 10 DECIMAL .BOTH
HEX FF DECIMAL 10 .BOTHThe second line switches back before the
10 and gets ten. A JavaScript literal carries its own base, so no line can affect how the next one is read; in Forth the base is state, which is why careful code puts DECIMAL back as soon as it can.Unused Results Pile Up
LOG-TWICE prints a number and leaves it on the stack for the caller, like a function that logs and returns its argument. The caller never uses the results, and DEPTH shows they are all still there.function logTwice(value) {
console.log(value);
return value;
}
logTwice(3);
logTwice(4);
console.log("done");: LOG-TWICE ( n -- n ) DUP . CR ;
3 LOG-TWICE
4 LOG-TWICE
.( done) CR
DEPTH . CRA JavaScript return value that nobody uses is discarded. A Forth result nobody uses stays on the stack, under whatever the next word expects to find — so in Forth, ignoring a result means writing
DROP.