PONYλM2Modula-2

C.CodeCompared.To/Forth

An interactive executable cheatsheet comparing C and Forth

C17 (GCC) Forth 2012 (WAForth 0.20.1)
Output & Running It
Hello, World
There is no main and no separate compile step. The Forth system reads source text word by word: : starts compiling a new word called GREET, ." compiles a string to print, CR compiles a newline, and ; finishes the definition. The last line is simply the name, so the interpreter runs it.
#include <stdio.h> static void greet(void) { printf("Hello, World!\n"); } int main(void) { greet(); return 0; }
: GREET ." Hello, World!" CR ; GREET
The compiler, the linker, the loader and the shell are all one program, and it is small enough to sit in the ROM of the device being programmed. That is how Forth was used on telescopes and satellites: a developer typed definitions over a serial line and ran them on the target at once, with no edit-compile-flash cycle at all.
printf and the Printing Words
Where printf uses a format string to decide how to show each argument, Forth has one word per interpretation: . prints a signed number, U. prints the same bits as unsigned, and EMIT prints a character code.
#include <stdio.h> int main(void) { int count = 42; unsigned int mask = 0xFFFFFFFFu; printf("%d\n", count); printf("%u\n", mask); printf("%c\n", 65); return 0; }
42 . CR -1 U. CR 65 EMIT CR
A format string is a way of telling the callee what types its variadic arguments have — which the compiler cannot check and -Wformat tries to. Forth has no types for a format to describe, so the choice of word is the format, and printing −1 with U. shows the same bits as 0xFFFFFFFFu.
The Stack Frame You Manage Yourself
Arguments Without a Frame
The three arguments are already on the data stack, in order, when the word starts. >R moves the offset to the return stack to get it out of the way, * multiplies the two values beneath it, R> brings the offset back, and + adds it. The stack-effect comment is the prototype.
#include <stdio.h> static int scale_and_offset(int value, int factor, int offset) { return value * factor + offset; } int main(void) { printf("%d\n", scale_and_offset(7, 3, 5)); return 0; }
: SCALE-AND-OFFSET ( value factor offset -- result ) >R * R> + ; 7 3 5 SCALE-AND-OFFSET . CR
A C calling convention puts the arguments in registers or a stack frame and gives each one a name. Forth has two stacks, both visible: the data stack for arguments and results, and the return stack for return addresses — which words may borrow for temporaries, as >R does here, provided everything is moved back before ;.
The Stack Operators
Four words cover most stack manipulation. DUP duplicates the top item, SWAP exchanges the top two, OVER copies the second item to the top and DROP discards the top. Each . pops and prints the top item, so a stack is printed from the top down.
#include <stdio.h> int main(void) { int second = 4, top = 9; printf("%d\n", top * top); /* DUP */ printf("%d %d\n", second, top); /* SWAP, printed top first */ printf("%d %d %d\n", second, top, second); /* OVER */ printf("%d\n", second); /* DROP the top */ return 0; }
9 DUP * . CR \ DUP copies the top item 4 9 SWAP . . CR \ SWAP exchanges the top two 4 9 OVER . . . CR \ OVER copies the second item to the top 4 9 DROP . CR \ DROP discards the top item
These four are each a single instruction or two in most Forth implementations, which is why Forth code can be nearly as fast as unoptimized C. A value used twice costs a DUP; in C, the compiler finds that out for itself.
Rewriting a Function That Uses Locals
Each local variable in the C function holds an intermediate result. In Forth, an intermediate result stays on the stack until it is used, so the locals disappear. Factoring the repeated DUP * into SQUARED is what makes the second word read clearly.
#include <stdio.h> static int hypotenuse_squared(int adjacent, int opposite) { int adjacent_squared = adjacent * adjacent; int opposite_squared = opposite * opposite; return adjacent_squared + opposite_squared; } int main(void) { printf("%d\n", hypotenuse_squared(3, 4)); return 0; }
: SQUARED ( n -- n*n ) DUP * ; : HYPOTENUSE-SQUARED ( adjacent opposite -- n ) SQUARED SWAP SQUARED + ; 3 4 HYPOTENUSE-SQUARED . CR
The Forth advice for a word that needs many locals is not to find a way to name them but to split the word, until each piece touches only two or three values. Forth 2012 does define local variables, {: … :}, in an optional word set — this page's Forth lacks it, and many Forth programmers never use it.
Words Instead Of Functions
Declaration Before Use
Like a C compiler, the Forth compiler reads the source once from top to bottom and must know a word before it meets a use of it. Unlike C, there is no prototype that promises a definition later: the definition itself must come first.
#include <stdio.h> static int twice(int value); /* a prototype lets main call it early */ int main(void) { printf("%d\n", twice(21)); return 0; } static int twice(int value) { return value * 2; }
: TWICE ( value -- value*2 ) 2 * ; 21 TWICE . CR
A C prototype exists so the compiler can check the call and the linker can find the function later. Forth has no separate link step — a word is compiled straight into memory and a call is resolved to its address immediately — so there is nothing for a forward declaration to say. For the rare genuine forward reference, such as mutual recursion, Forth uses a DEFERred word.
Recursion
RECURSE calls the word being defined, whose name is not yet visible. ?DUP duplicates the second number only if it is nonzero, and TUCK copies the top item underneath the second, turning first second into second first second, ready for MOD.
#include <stdio.h> static unsigned greatest_common_divisor(unsigned first, unsigned second) { return second == 0 ? first : greatest_common_divisor(second, first % second); } int main(void) { printf("%u\n", greatest_common_divisor(1071, 462)); return 0; }
: GREATEST-COMMON-DIVISOR ( first second -- divisor ) ?DUP IF TUCK MOD RECURSE THEN ; 1071 462 GREATEST-COMMON-DIVISOR . CR
When the second number reaches zero, ?DUP leaves only the answer and the IF body is skipped. The recursive call is the last thing the word does, but Forth does not promise to turn it into a jump; like C, a deep enough recursion runs out of return stack.
Output Parameters
A word returns as many values as it leaves on the stack. The built-in /MOD leaves the remainder and then the quotient; DIVIDE swaps them into quotient-then-remainder order.
#include <stdio.h> static void divide(int dividend, int divisor, int *quotient, int *remainder) { *quotient = dividend / divisor; *remainder = dividend % divisor; } int main(void) { int quotient, remainder; divide(47, 6, &quotient, &remainder); printf("%d %d\n", quotient, remainder); return 0; }
: DIVIDE ( dividend divisor -- quotient remainder ) /MOD SWAP ; 47 6 DIVIDE SWAP . . CR
C needs pointer parameters (or a struct) to return two values, and the caller has to allocate somewhere for them to land. Forth returns both the same way it returns one, with nothing allocated and nothing addressed. The final SWAP . . prints the quotient first to match the C output.
Cells, Integers & Overflow
sizeof and CELLS
A Forth cell is the size of one item on the stack, and CELLS converts a count of cells into a count of address units — bytes, on every current machine. CHARS does the same for characters.
#include <stdio.h> #include <stdint.h> int main(void) { printf("%zu %zu\n", sizeof(int32_t), sizeof(char)); printf("%zu\n", 10 * sizeof(int32_t)); return 0; }
1 CELLS . 1 CHARS . CR 10 CELLS . CR
CELLS is sizeof for the one type Forth has. The C column names int32_t to match this page's Forth, whose cells are 32 bits; on a 64-bit Forth 1 CELLS is 8, and portable Forth code writes CELLS rather than 4 for the same reason portable C writes sizeof.
Overflow Is Defined
Forth arithmetic is two's-complement arithmetic on cells, and it wraps. Adding one to the largest positive 32-bit cell gives the most negative one, and the same bits read with U. are an unsigned number.
#include <stdio.h> #include <stdint.h> #include <limits.h> int main(void) { uint32_t largest = INT32_MAX; int32_t wrapped = (int32_t)(largest + 1u); printf("%d\n", wrapped); printf("%u\n", (uint32_t)-1); return 0; }
2147483647 1+ . CR -1 U. CR
In C, INT32_MAX + 1 on a signed type is undefined behavior that an optimizer is entitled to assume never happens, which is why the C column does the addition on unsigned values and converts. Forth has no signed or unsigned types, only signed and unsigned words — . and U., < and U< — applied to the same bits, so wrapping is simply what happens.
Division Rounds Toward Zero, If You Ask
SM/REM ("symmetric") truncates toward zero, as C99 requires of / and %. It divides a double-width number, so S>D sign-extends the dividend first, and it leaves the remainder under the quotient.
#include <stdio.h> int main(void) { printf("%d %d\n", -17 / 5, -17 % 5); return 0; }
: C-STYLE-DIVIDE ( n d -- remainder quotient ) >R S>D R> SM/REM ; -17 5 C-STYLE-DIVIDE . . CR
C89 let / round either way for negative operands, and C99 fixed it to truncate. Forth still leaves plain / and MOD to the implementation, and names both behaviors explicitly: SM/REM for C's, and FM/MOD for floored division, whose remainder takes the divisor's sign.
Fixed-Point Arithmetic
*/ multiplies by the second number and divides by the third, keeping the intermediate product in double width so that it cannot overflow. That is the whole of fixed-point scaling: convert an ADC reading to millivolts without ever holding a fraction.
#include <stdio.h> #include <stdint.h> int main(void) { int32_t millivolts = 3300; int32_t reading = 2911; /* out of 4095 */ int32_t measured = (int32_t)((int64_t)reading * millivolts / 4095); printf("%d mV\n", measured); return 0; }
: MEASURED ( reading -- millivolts ) 3300 4095 */ ; : DEMO 2911 MEASURED . ." mV" CR ; DEMO
The C version has to widen to int64_t by hand for the product and narrow again afterward, and forgetting the cast is a classic overflow. */ builds the widening in, which is why it is one of the most used words in embedded Forth, where there is often no floating-point unit at all.
Bit Masks
The operators are the same ones C has: OR, AND, INVERT for ~, LSHIFT and RSHIFT. BIT turns a bit number into a mask, and the flags value never leaves the stack while each mask is combined with it.
#include <stdio.h> int main(void) { unsigned flags = 0x0; flags |= 1u << 3; /* set bit 3 */ flags |= 1u << 0; /* set bit 0 */ flags &= ~(1u << 0); /* clear bit 0 */ printf("%u %u\n", flags, (flags >> 3) & 1u); return 0; }
: BIT ( n -- mask ) 1 SWAP LSHIFT ; : DEMO 0 3 BIT OR 0 BIT OR 0 BIT INVERT AND DUP . 3 RSHIFT 1 AND . CR ; DEMO
A small word like BIT is what C would write as a macro, #define BIT(n) (1u << (n)). In Forth there is no difference between a macro and a function for something this small, and no parentheses to forget around the argument.
Pointers Without Types
* Is @, and *p = v Is !
VARIABLE COUNTER allocates one cell and defines COUNTER as a word that pushes that cell's address — so the name means &counter, not counter. ! stores through an address, @ fetches through one, and +! adds through one.
#include <stdio.h> int main(void) { int counter = 0; int *address = &counter; *address = 41; *address += 1; printf("%d\n", *address); return 0; }
VARIABLE COUNTER 41 COUNTER ! 1 COUNTER +! COUNTER @ . CR
Every Forth variable behaves like a C pointer that is dereferenced by hand. There is no implicit load or store anywhere in the language, which is why a Forth programmer reading code always knows exactly where memory is touched.
Pointer Arithmetic Is Not Scaled
An address is a plain number, so adding 2 to it moves 2 bytes. To reach the element at index 2, the offset has to be written in bytes: 2 CELLS +.
#include <stdio.h> #include <stdint.h> int main(void) { int32_t values[3] = { 100, 200, 300 }; int32_t *element = values; element += 2; /* moves 2 * sizeof(int32_t) bytes */ printf("%d\n", *element); printf("%td\n", (char *)element - (char *)values); return 0; }
CREATE VALUES 100 , 200 , 300 , VALUES 2 CELLS + @ . CR 2 CELLS . CR
C scales element += 2 by sizeof(int32_t) because a C pointer carries its target type. A Forth address carries nothing, so the scaling is explicit — which also means VALUES 2 + @, missing the CELLS, is not a type error but a fetch from the middle of the first element.
Reading Individual Bytes
@ fetches a whole cell and C@ fetches a single byte (a character, in Forth's vocabulary); both work on any address. .BYTES walks four byte addresses and prints each in hexadecimal.
#include <stdio.h> #include <stdint.h> int main(void) { uint32_t word = 0x11223344u; uint8_t *byte = (uint8_t *)&word; printf("%x %x %x %x\n", byte[0], byte[1], byte[2], byte[3]); return 0; }
VARIABLE WORD-VALUE HEX 11223344 DECIMAL WORD-VALUE ! : .BYTES ( address -- ) HEX 4 0 DO DUP I + C@ . LOOP DECIMAL DROP CR ; WORD-VALUE .BYTES
Both columns print 44 33 22 11: the machine underneath is little-endian. In C, reading an object's bytes needs a cast to a character pointer, which is one of the few aliasing conversions the standard permits; in Forth there is nothing to cast, because an address was never a pointer to anything in particular.
Allocation Without free
HERE pushes the address of the next free byte in Forth's dictionary space, and ALLOT moves that point forward by a number of bytes. Taking HERE before 64 ALLOT gives the address of a fresh 64-byte block, which CONSTANT BUFFER names.
#include <stdio.h> #include <stdlib.h> int main(void) { unsigned char *buffer = malloc(64); if (buffer == NULL) return 1; buffer[0] = 7; printf("%d\n", buffer[0]); free(buffer); return 0; }
HERE 64 ALLOT CONSTANT BUFFER 7 BUFFER C! BUFFER C@ . CR
This is a bump allocator, and it is the core of Forth's memory model: space is claimed in order and never individually freed. Most embedded C code on small microcontrollers avoids malloc for the same reasons — fragmentation and unpredictable timing — and allocates statically at startup, which is exactly what this is. Forth also offers ALLOCATE and FREE in an optional word set.
memset and memcpy
FILL takes an address, a count and a byte, like memset with the last two arguments swapped. MOVE takes a source, a destination and a count — the source first, the reverse of memcpy. S" ok" supplies the source address and length, so SWAP puts the destination between them.
#include <stdio.h> #include <string.h> int main(void) { char line[12]; memset(line, '-', sizeof line); memcpy(line + 4, "ok", 2); printf("%.12s\n", line); return 0; }
CREATE LINE 12 ALLOT : DEMO LINE 12 [CHAR] - FILL S" ok" LINE 4 + SWAP MOVE LINE 12 TYPE CR ; DEMO
MOVE behaves like memmove, handling overlapping regions correctly, so there is no Forth equivalent of choosing the wrong one of memcpy and memmove. TYPE prints exactly 12 bytes, just as %.12s does, since neither buffer ends in a zero.
Arrays & Structs Are Offsets
An Indexing Word
CREATE SAMPLES 8 CELLS ALLOT is static int32_t samples[8]. SAMPLE is the subscript operator, written out: it turns an index into an address, which ! and @ then use.
#include <stdio.h> #include <stdint.h> static int32_t samples[8]; int main(void) { for (int index = 0; index < 8; index++) samples[index] = index * index; int32_t sum = 0; for (int index = 0; index < 8; index++) sum += samples[index]; printf("%d\n", sum); return 0; }
CREATE SAMPLES 8 CELLS ALLOT : SAMPLE ( index -- address ) CELLS SAMPLES + ; : FILL-SAMPLES 8 0 DO I DUP * I SAMPLE ! LOOP ; : SUM-SAMPLES ( -- sum ) 0 8 0 DO I SAMPLE @ + LOOP ; FILL-SAMPLES SUM-SAMPLES . CR
Defining the indexing word once is what keeps the scaling out of every use. C's samples[index] is defined as *(samples + index), and I SAMPLE @ is that definition spelled out with the scaling made visible.
A struct
Each field is a word that adds its offset to a struct's address, and the struct size is a constant. Applying a field word to 0 yields the bare offset, which is exactly what offsetof reports.
#include <stdio.h> #include <stdint.h> #include <stddef.h> struct sensor { int32_t identifier; int32_t minimum; int32_t maximum; }; int main(void) { struct sensor probe = { 7, -40, 125 }; printf("%d %d\n", probe.identifier, probe.maximum - probe.minimum); printf("%zu %zu\n", offsetof(struct sensor, maximum), sizeof(struct sensor)); return 0; }
: >IDENTIFIER ( sensor -- address ) ; : >MINIMUM ( sensor -- address ) 1 CELLS + ; : >MAXIMUM ( sensor -- address ) 2 CELLS + ; 3 CELLS CONSTANT SENSOR-SIZE CREATE PROBE 7 , -40 , 125 , PROBE >IDENTIFIER @ . PROBE >MAXIMUM @ PROBE >MINIMUM @ - . CR 0 >MAXIMUM . SENSOR-SIZE . CR
0 >MAXIMUM is the same trick as the classic C definition of offsetof, which takes a member's address from a null pointer. Nothing ties the field words to PROBE, so they work on any three cells anywhere — the flexibility and the danger of a C cast, without the cast.
Generating the Field Offsets
Writing offsets by hand is error-prone, so a defining word computes them. FIELD takes the running offset and a field size: it creates a word that stores the current offset, then adds the size to get the next one. Each created word, when used, adds its stored offset to an address. The offset left at the end is the struct size.
#include <stdio.h> #include <stddef.h> #include <stdint.h> struct packet { uint8_t kind; uint8_t flags; uint16_t length; int32_t payload; }; int main(void) { printf("%zu %zu %zu %zu\n", offsetof(struct packet, kind), offsetof(struct packet, flags), offsetof(struct packet, length), offsetof(struct packet, payload)); printf("%zu\n", sizeof(struct packet)); return 0; }
: FIELD ( offset size "name" -- offset' ) CREATE OVER , + DOES> ( address -- address+offset ) @ + ; 0 1 FIELD >KIND 1 FIELD >FLAGS 2 FIELD >LENGTH 4 FIELD >PAYLOAD CONSTANT PACKET-SIZE 0 >KIND . 0 >FLAGS . 0 >LENGTH . 0 >PAYLOAD . CR PACKET-SIZE . CR
This is what a C compiler does when it lays out a struct, done by the program instead — without padding, since these sizes are chosen to line up. Forth 2012 standardizes the same idea as +FIELD; the whole mechanism is three lines because CREATE and DOES> give every Forth programmer the power C reserves for its compiler.
Strings Carry Their Length
No Terminator
A Forth string is two numbers on the stack: the address of its first byte and its length. S" leaves both, TYPE prints exactly that many bytes, and NIP discards the address to keep the length.
#include <stdio.h> #include <string.h> int main(void) { const char *greeting = "Hello"; printf("%s\n", greeting); printf("%zu\n", strlen(greeting)); return 0; }
: GREETING ( -- address length ) S" Hello" ; GREETING TYPE CR GREETING NIP . CR
C marks the end of a string with a zero byte, so strlen has to scan for it and a string cannot contain a zero. A Forth string's length travels with its address, so finding the length costs nothing and any byte value is allowed — the same design as Go's and Rust's slices, arrived at in 1970.
Substrings Without Copying
A substring is another address and length that point into the same bytes. Dropping the original length, moving the address 5 bytes forward and supplying a new length of 5 selects local.
#include <stdio.h> int main(void) { const char *path = "/usr/local/bin"; printf("%.5s\n", path + 5); return 0; }
: DEMO S" /usr/local/bin" DROP 5 + 5 TYPE CR ; DEMO
C can only do this with a precision in the format string or by writing a terminator into the buffer, because a char * alone cannot say where to stop. In Forth every string is already a slice, so taking part of one is address arithmetic and nothing is copied.
Counted Strings
C" compiles a counted string: a length byte followed by the characters, referred to by the single address of the length byte. COUNT converts it into the usual address-and-length pair by fetching the length and stepping past it.
#include <stdio.h> /* A length-prefixed string, the way Pascal stored them. */ static const unsigned char label[] = { 5, 'm', 'o', 't', 'o', 'r' }; int main(void) { printf("%d %.*s\n", label[0], label[0], (const char *)label + 1); return 0; }
: LABEL ( -- address ) C" motor" ; : DEMO LABEL C@ . LABEL COUNT TYPE CR ; DEMO
Counted strings are the older Forth format, still used where a string has to be stored in memory or passed as one value, and they limit a string to 255 bytes. Address-and-length pairs replaced them for everything else, much as C code increasingly passes a pointer and a size_t instead of relying on the terminator.
Comparing Strings
With lengths known in advance, two strings of different lengths can be rejected without looking at a single byte. Otherwise each pair of bytes is compared, and UNLOOP clears the loop's counters before an early EXIT. 0<> 1 AND turns Forth's −1 into the 1 that C's comparison produces.
#include <stdio.h> #include <string.h> int main(void) { printf("%d\n", strcmp("north", "north") == 0); printf("%d\n", strcmp("north", "south") == 0); return 0; }
: SAME-BYTES? ( address1 length1 address2 length2 -- flag ) ROT OVER <> IF DROP 2DROP FALSE EXIT THEN 0 ?DO OVER I + C@ OVER I + C@ <> IF 2DROP FALSE UNLOOP EXIT THEN LOOP 2DROP TRUE ; : DEMO S" north" S" north" SAME-BYTES? 0<> 1 AND . CR S" north" S" south" SAME-BYTES? 0<> 1 AND . CR ; DEMO
strcmp has to walk both strings to their terminators; here the length check makes the common "different length" case free. Many Forths provide COMPARE, which orders strings like memcmp, in the optional string word set.
Formatting a Number Into a Buffer
Pictured numeric output converts a number into text in a buffer the system owns. <# starts, each # divides by the base and adds one digit — a zero once the number runs out — and #> leaves the address and length of the result. The 0 widens the number to double width first.
#include <stdio.h> int main(void) { char text[16]; int length = snprintf(text, sizeof text, "%05d", 1234); printf("%s %d\n", text, length); return 0; }
: FIVE-DIGITS ( n -- address length ) 0 <# # # # # # #> ; : DEMO 1234 FIVE-DIGITS DUP >R TYPE SPACE R> . CR ; DEMO
Five #s are %05d. The digits are generated from the right, by repeated division, which is how every itoa works inside; Forth simply exposes the loop as words, so a program can insert a decimal point with HOLD or a sign with SIGN at any position.
Control Flow
if and else
The condition is computed first and leaves a flag; IF consumes it. ELSE separates the branches and THEN closes the structure. The first test DUPs the temperature so the second test still has it, and drops the copy when it matches.
#include <stdio.h> static void classify(int temperature) { if (temperature > 100) printf("boiling\n"); else if (temperature < 0) printf("freezing\n"); else printf("liquid\n"); } int main(void) { classify(120); classify(-5); classify(40); return 0; }
: CLASSIFY ( temperature -- ) DUP 100 > IF DROP ." boiling" CR EXIT THEN 0< IF ." freezing" ELSE ." liquid" THEN CR ; 120 CLASSIFY -5 CLASSIFY 40 CLASSIFY
IF … ELSE … THEN compiles to exactly the conditional branch and jump a C compiler emits for if … else; the words are that direct. They only work inside a definition, because at the top level there is no code being compiled for a branch to jump over.
for and while
5 0 DO … LOOP is for (int index = 0; index < 5; index++) with the limit written first, and I reads the index. BEGIN … WHILE … REPEAT is a while loop: the flag computed before WHILE decides whether the body runs again.
#include <stdio.h> int main(void) { for (int index = 0; index < 5; index++) printf("%d ", index); printf("\n"); unsigned value = 200; int halvings = 0; while (value > 1) { value >>= 1; halvings++; } printf("%d\n", halvings); return 0; }
: COUNT-UP 5 0 DO I . LOOP CR ; : HALVINGS ( value -- count ) 0 SWAP BEGIN DUP 1 > WHILE 1 RSHIFT SWAP 1+ SWAP REPEAT DROP ; COUNT-UP 200 HALVINGS . CR
A DO loop keeps its index and limit on the return stack, much as a C compiler keeps a loop counter in a register. That is also why a word must not leave a DO loop with EXIT without first calling UNLOOP: the return stack would still hold the loop's counters instead of the address to return to.
The Empty Loop That Is Not Empty
?DO skips the loop when the start equals the limit, which is the behavior a C for loop has. Plain DO does not check first.
#include <stdio.h> static void dashes(int count) { for (int index = 0; index < count; index++) putchar('-'); printf("|\n"); } int main(void) { dashes(4); dashes(0); return 0; }
: DASHES ( count -- ) 0 ?DO [CHAR] - EMIT LOOP ." |" CR ; 4 DASHES 0 DASHES
DO is a do-while: it runs the body before testing, so 0 0 DO would count upward, wrap through all 232 possible values and only then stop. The C loop tests before the first iteration; ?DO is the Forth word that does the same.
Function Pointers
Calling Through a Pointer
' reads the name that follows and pushes that word's execution token, and EXECUTE calls the word a token identifies. Inside a definition, ['] CUBE is written instead, so the name is read when the definition is compiled.
#include <stdio.h> static int cube(int value) { return value * value * value; } int main(void) { int (*operation)(int) = cube; printf("%d\n", operation(5)); return 0; }
: CUBE ( value -- value^3 ) DUP DUP * * ; ' CUBE 5 SWAP EXECUTE . CR
An execution token is a function pointer without a signature: the compiler cannot warn about calling it with the wrong arguments, because no word has a declared argument list for it to compare against. Calling through a token costs one indirect call, the same as in C.
A Jump Table
' STOP , stores the execution token of STOP in the next cell of the array. COMMAND fetches the token at an index and executes it — the whole dispatch mechanism of a bytecode interpreter or a command parser, in one line.
#include <stdio.h> static void stop(void) { printf("stop\n"); } static void start(void) { printf("start\n"); } static void reset(void) { printf("reset\n"); } static void (*const commands[])(void) = { stop, start, reset }; int main(void) { commands[1](); commands[2](); commands[0](); return 0; }
: STOP ." stop" CR ; : START ." start" CR ; : RESET ." reset" CR ; CREATE COMMANDS ' STOP , ' START , ' RESET , : COMMAND ( index -- ) CELLS COMMANDS + @ EXECUTE ; 1 COMMAND 2 COMMAND 0 COMMAND
A jump table is the reason a C switch over dense values is fast, and it is the reason Forth needs no switch for dispatch: the table is ordinary data. A real one would check the index first, since, exactly as in C, an index past the end fetches whatever is stored there and jumps to it.
A Comparison Callback
SORT-ITEMS takes a count and the execution token of a comparison, and swaps any neighbors the comparison says are out of order. Passing the built-in > sorts in descending order, because a word is a function pointer whether it is built in or not. EXCHANGE swaps the contents of two addresses.
#include <stdio.h> static int descending(int first, int second) { return first > second; } static void bubble_sort(int *items, int count, int (*before)(int, int)) { for (int pass = 0; pass < count - 1; pass++) for (int index = 0; index < count - 1 - pass; index++) if (before(items[index + 1], items[index])) { int held = items[index]; items[index] = items[index + 1]; items[index + 1] = held; } } int main(void) { int items[] = { 3, 9, 1, 7, 5 }; bubble_sort(items, 5, descending); for (int index = 0; index < 5; index++) printf("%d ", items[index]); printf("\n"); return 0; }
CREATE ITEMS 3 , 9 , 1 , 7 , 5 , : ITEM ( index -- address ) CELLS ITEMS + ; : EXCHANGE ( address1 address2 -- ) 2DUP @ SWAP @ ROT ! SWAP ! ; : SORT-ITEMS ( count xt -- ) SWAP 1- DUP 0 ?DO \ xt last (I is the pass) DUP I - 0 ?DO \ xt last (I is the position) I 1+ ITEM @ I ITEM @ 3 PICK EXECUTE IF I ITEM I 1+ ITEM EXCHANGE THEN LOOP LOOP 2DROP ; : DEMO 5 ['] > SORT-ITEMS 5 0 DO I ITEM @ . LOOP CR ; DEMO
The comparison token rides on the data stack, under the last index, and 3 PICK copies it up for each comparison. 🚨 It cannot be parked on the return stack with >R and read inside the loops with R@, the way a C programmer might reach for a register: each DO keeps its index and limit on that same return stack, so R@ would return a loop counter and EXECUTE would jump to it. Inside the inner loop I is the position; J would be the pass.
The Preprocessor Is the Language
#define and CONSTANT
CONSTANT takes a number from the stack and defines a word that pushes it. Because the value comes from the stack, it can be the result of any computation that runs before the definition.
#include <stdio.h> #define BAUD_RATE 9600 #define BITS_PER_BYTE 10 /* start + 8 data + stop */ int main(void) { printf("%d\n", BAUD_RATE / BITS_PER_BYTE); return 0; }
9600 CONSTANT BAUD-RATE 10 CONSTANT BITS-PER-BYTE \ start + 8 data + stop BAUD-RATE BITS-PER-BYTE / . CR
A #define is text substitution, resolved before the compiler sees the code and invisible to a debugger. A Forth constant is a real word in the dictionary, which the interactive system can print, test and use immediately — and there is no separate preprocessor language with its own rules to learn.
A Function-Like Macro
ROT brings the value to the top, MIN keeps the smaller of it and the high bound, and MAX keeps the larger of that and the low bound.
#include <stdio.h> #define CLAMP(value, low, high) \ ((value) < (low) ? (low) : (value) > (high) ? (high) : (value)) int main(void) { printf("%d %d %d\n", CLAMP(-5, 0, 100), CLAMP(50, 0, 100), CLAMP(900, 0, 100)); return 0; }
: CLAMP ( value low high -- clamped ) ROT MIN MAX ; -5 0 100 CLAMP . 50 0 100 CLAMP . 900 0 100 CLAMP . CR
The C macro exists to avoid a function call and to work on any integer type, and it pays with parentheses around every argument and with evaluating value up to three times. The Forth word takes its arguments from the stack, so each is computed exactly once, and with no types there is only one version to write. A Forth word this small is often compiled inline by an optimizing Forth anyway.
Computing at Compile Time
Inside a definition, [ stops compiling and starts running words, and ] resumes compiling. The multiplication between them runs once, while TIMEOUT-TICKS is compiled, and LITERAL compiles the result into the word as a constant.
#include <stdio.h> enum { TICKS_PER_SECOND = 1000, TIMEOUT_TICKS = 30 * 60 * TICKS_PER_SECOND }; int main(void) { printf("%d\n", TIMEOUT_TICKS); return 0; }
1000 CONSTANT TICKS-PER-SECOND : TIMEOUT-TICKS ( -- ticks ) [ 30 60 * TICKS-PER-SECOND * ] LITERAL ; TIMEOUT-TICKS . CR
C restricts compile-time computation to constant expressions — or to templates and constexpr in its descendants. Between [ and ] any Forth word at all can run, including ones the program itself defined a moment earlier.
A Macro That Is Real Code
An IMMEDIATE word runs while the definition around it is being compiled, instead of being compiled into it. POSTPONE compiles a word into that definition. So when BEEPS is compiled, TIMES writes 0 ?DO into it and END-TIMES writes LOOP: two new keywords for a counted loop. A number is not a word and cannot be postponed, so 0 POSTPONE LITERAL is how TIMES compiles the 0.
#include <stdio.h> /* Repeat a statement a fixed number of times, as a macro. */ #define REPEAT_TIMES(count) for (int repetition = 0; repetition < (count); repetition++) int main(void) { REPEAT_TIMES(3) printf("beep "); printf("\n"); return 0; }
: TIMES 0 POSTPONE LITERAL POSTPONE ?DO ; IMMEDIATE : END-TIMES POSTPONE LOOP ; IMMEDIATE : BEEPS ( count -- ) TIMES ." beep " END-TIMES CR ; 3 BEEPS
The C macro is text pasted in before compilation, and it quietly declares a variable named repetition that could collide with one of yours. The Forth version runs as real code inside the compiler and inserts compiled loop instructions — no text, no names to collide, and it can compute anything it likes along the way. IF, DO and ; are immediate words defined in exactly this way.
Embedded Idioms
A Memory-Mapped Register
A hardware register is an address, and in Forth an address is just a number that @ and ! accept — so a register is a CONSTANT holding its address. The example uses a VARIABLE instead so it has somewhere real to write. BIT-SET and BIT-CLEAR are read-modify-write words for any register.
#include <stdio.h> #include <stdint.h> /* On a microcontroller this would be a fixed address such as 0x40020014. */ static uint32_t simulated_output_register; #define GPIO_OUTPUT (*(uint32_t *)&simulated_output_register) #define LED_PIN 5u int main(void) { GPIO_OUTPUT |= 1u << LED_PIN; /* LED on */ printf("%u\n", GPIO_OUTPUT); GPIO_OUTPUT &= ~(1u << LED_PIN); /* LED off */ printf("%u\n", GPIO_OUTPUT); return 0; }
\ On a microcontroller this would be HEX 40020014 CONSTANT GPIO-OUTPUT VARIABLE GPIO-OUTPUT 5 CONSTANT LED-PIN : BIT-SET ( mask address -- ) DUP @ ROT OR SWAP ! ; : BIT-CLEAR ( mask address -- ) DUP @ ROT INVERT AND SWAP ! ; : LED-ON 1 LED-PIN LSHIFT GPIO-OUTPUT BIT-SET ; : LED-OFF 1 LED-PIN LSHIFT GPIO-OUTPUT BIT-CLEAR ; LED-ON GPIO-OUTPUT @ . CR LED-OFF GPIO-OUTPUT @ . CR
C needs a cast to turn a number into an address and volatile to stop the optimizer from caching what it reads. Forth needs neither: nothing is optimized away behind your back, and at a live Forth prompt on a board, LED-ON typed over the serial line lights the LED at once. That immediacy is why Forth is still used to bring up new hardware.
A Lookup Table in ROM
C, stores one byte and moves on, so a CREATE followed by a row of C, is a byte table. Switching to HEX before the table lets the patterns be written exactly as a datasheet shows them. SEGMENT needs no scaling, because each entry is one byte.
#include <stdio.h> #include <stdint.h> /* Segments a-g for the digits 0-9 on a seven-segment display. */ static const uint8_t SEGMENTS[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; int main(void) { printf("%X %X %X\n", SEGMENTS[0], SEGMENTS[4], SEGMENTS[8]); return 0; }
HEX CREATE SEGMENTS 3F C, 06 C, 5B C, 4F C, 66 C, 6D C, 7D C, 07 C, 7F C, 6F C, DECIMAL : SEGMENT ( digit -- pattern ) SEGMENTS + C@ ; : .HEX ( n -- ) HEX . DECIMAL ; 0 SEGMENT .HEX 4 SEGMENT .HEX 8 SEGMENT .HEX CR
The C table goes into read-only memory because it is const and static. On a Forth system that compiles to flash, the dictionary itself — code and tables alike — is what gets written to ROM, so a table defined like this is already where a C linker would have placed it.
A Ring Buffer
ADVANCE increments the index stored at an address and wraps it with MOD, and it works for both HEAD and TAIL because it is given the variable's address. PUT stores a byte at the head and TAKE fetches the byte at the tail.
#include <stdio.h> #include <stdint.h> #define CAPACITY 4u static uint8_t storage[CAPACITY]; static unsigned head, tail; static void put(uint8_t value) { storage[head] = value; head = (head + 1) % CAPACITY; } static uint8_t take(void) { uint8_t value = storage[tail]; tail = (tail + 1) % CAPACITY; return value; } int main(void) { put(10); put(20); put(30); printf("%d ", take()); put(40); put(50); int second = take(); /* one call per statement: the order of a */ int third = take(); /* call's own arguments is unspecified */ int fourth = take(); printf("%d %d %d\n", second, third, fourth); return 0; }
4 CONSTANT CAPACITY CREATE STORAGE CAPACITY ALLOT VARIABLE HEAD 0 HEAD ! VARIABLE TAIL 0 TAIL ! : ADVANCE ( address -- ) DUP @ 1+ CAPACITY MOD SWAP ! ; : PUT ( byte -- ) STORAGE HEAD @ + C! HEAD ADVANCE ; : TAKE ( -- byte ) STORAGE TAIL @ + C@ TAIL ADVANCE ; 10 PUT 20 PUT 30 PUT TAKE . 40 PUT 50 PUT TAKE . TAKE . TAKE . CR
Like the C version, this buffer does not check for being full or empty, which keeps the example the same size in both columns. Passing the variable's address to ADVANCE, rather than its value, is Forth's everyday version of passing a pointer so the callee can update it.
A Checksum Over a Buffer
CHECKSUM takes a buffer as an address and length and folds every byte into a running XOR. 0 SWAP puts the starting sum between the address and the length, so ?DO consumes the length and the loop body finds the address under the sum. The result is printed as exactly two hexadecimal digits with pictured output, as %02X does.
#include <stdio.h> #include <stdint.h> static uint8_t checksum(const uint8_t *bytes, unsigned length) { uint8_t sum = 0; for (unsigned index = 0; index < length; index++) sum ^= bytes[index]; return sum; } int main(void) { const uint8_t frame[] = { 0x02, 0x10, 0xA5, 0x3C }; printf("%02X\n", checksum(frame, sizeof frame)); return 0; }
HEX CREATE FRAME 02 C, 10 C, A5 C, 3C C, DECIMAL : CHECKSUM ( address length -- byte ) 0 SWAP 0 ?DO OVER I + C@ XOR LOOP NIP ; : DEMO FRAME 4 CHECKSUM HEX 0 <# # # #> TYPE DECIMAL CR ; DEMO
An address-and-length pair is Forth's standard way to pass a buffer, and it is the signature this C function already has. The only difference is where sum lives: in a local in C, and on the stack, under the address, in Forth.
Errors Without errno
Returning a Status
READ-SENSOR leaves a value and TRUE on success, or only FALSE on failure. WITHIN checks 0 <= channel < 4 in one word. The caller tests the flag with IF before touching the value underneath.
#include <stdio.h> #include <stdbool.h> static bool read_sensor(int channel, int *value) { if (channel < 0 || channel > 3) return false; *value = 500 + channel * 10; return true; } int main(void) { int value; for (int channel = 2; channel <= 5; channel += 3) { if (read_sensor(channel, &value)) printf("channel %d: %d\n", channel, value); else printf("channel %d: invalid\n", channel); } return 0; }
: READ-SENSOR ( channel -- value true | false ) DUP 0 4 WITHIN IF 10 * 500 + TRUE ELSE DROP FALSE THEN ; : REPORT ( channel -- ) DUP ." channel " 0 <# #S #> TYPE ." : " READ-SENSOR IF . ELSE ." invalid" THEN CR ; 2 REPORT 5 REPORT
The C function returns its status and writes the result through a pointer; the Forth word returns both on the stack, and returns a different number of items depending on the outcome. That is legal and common in Forth, and it is exactly the kind of contract only the stack-effect comment records.
assert and ABORT"
ABORT" takes a flag; if it is true, it prints the message, empties both stacks and returns control to the outermost interpreter. The DUP keeps the percentage on the stack for the normal case.
#include <stdio.h> #include <stdlib.h> static void set_duty_cycle(int percent) { if (percent > 100) { fprintf(stderr, "duty cycle above 100%%\n"); exit(1); } printf("duty %d\n", percent); } int main(void) { set_duty_cycle(75); set_duty_cycle(150); set_duty_cycle(20); return 0; }
: SET-DUTY-CYCLE ( percent -- ) DUP 100 > ABORT" duty cycle above 100%" ." duty " . CR ; 75 SET-DUTY-CYCLE 150 SET-DUTY-CYCLE 20 SET-DUTY-CYCLE
In C, exit ends the process. ABORT" ends only the current command: on an interactive Forth the system is back at its prompt with every word still defined, which is what makes aborting a safe way to handle a mistake typed at a live board. Both programs stop before the third call, and both exit with a failure, which is expected here.
setjmp and CATCH
CATCH executes a word from its execution token and leaves 0 if the word returned normally. THROW with a nonzero number unwinds to the most recent CATCH, restores the data stack to the depth it had there, and leaves that number instead.
#include <stdio.h> #include <setjmp.h> static jmp_buf recovery; static void parse_field(int value) { if (value < 0) longjmp(recovery, 2); printf("parsed %d\n", value); } int main(void) { int values[] = { 12, -1 }; for (int index = 0; index < 2; index++) { int error = setjmp(recovery); if (error == 0) parse_field(values[index]); else printf("error %d\n", error); } return 0; }
2 CONSTANT NEGATIVE-FIELD : PARSE-FIELD ( value -- ) DUP 0< IF NEGATIVE-FIELD THROW THEN ." parsed " . CR ; : TRY-FIELD ( value -- ) ['] PARSE-FIELD CATCH ?DUP IF ." error " . CR DROP THEN ; 12 TRY-FIELD -1 TRY-FIELD
This is setjmp/longjmp with the two sharp edges removed: the stack depth is restored for you, so the argument PARSE-FIELD consumed comes back as a placeholder that the DROP removes, and the return stack is unwound properly instead of being abandoned. CATCH and THROW are the optional exception word set, which this page's Forth does not include.
Gotchas For C Developers
@ Reads a Whole Cell
C@ fetches one byte, and @ fetches a whole cell starting at the same address. Four bytes of 7, 1, 0, 0 read as a little-endian cell are 7 + 256.
#include <stdio.h> #include <stdint.h> int main(void) { uint8_t bytes[4] = { 7, 1, 0, 0 }; printf("%u\n", bytes[0]); uint32_t whole = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8; printf("%u\n", whole); return 0; }
CREATE BYTES 7 C, 1 C, 0 C, 0 C, BYTES C@ . CR BYTES @ . CR
Nothing in Forth says what size the data at an address is, so choosing @ where C@ was meant is not a type error — it silently reads 263. In C, dereferencing a uint8_t * can only ever read one byte, and the C column has to assemble the whole 32-bit value by hand to get the same 263.
Signed and Unsigned Comparison
< compares two cells as signed numbers and U< compares the same bits as unsigned. −1 is less than 1 when signed, and the largest possible value when unsigned. 0<> 1 AND turns Forth's −1 flag into C's 1.
#include <stdio.h> int main(void) { int negative = -1; unsigned small = 1u; printf("%d\n", negative < 1); printf("%d\n", (unsigned)negative < small); return 0; }
-1 1 < 0<> 1 AND . CR -1 1 U< 0<> 1 AND . CR
C decides which comparison to perform from the operand types, and mixing an int with an unsigned silently converts the int — a famous source of bugs, and the reason for -Wsign-compare. Forth makes the programmer choose the word each time, which is more to remember and impossible to get implicitly.
Defining Something Moves HERE
Code and data share one region of memory, filled in the order the source defines things. TWICE is compiled first, so its code comes before TABLE, whose two values are then laid down with ,.
#include <stdio.h> #include <stdint.h> static int32_t table[2]; static int32_t twice(int32_t value) { return value * 2; } int main(void) { table[0] = 11; table[1] = 22; printf("%d %d %d\n", table[0], table[1], twice(table[1])); return 0; }
: TWICE ( n -- n*2 ) 2 * ; CREATE TABLE 11 , 22 , TABLE @ . TABLE CELL+ @ . TABLE CELL+ @ TWICE . CR
A C toolchain sorts functions into .text and arrays into .data no matter how the file is ordered. Forth does not separate them, so defining a word between a CREATE and the ,s meant for it puts the word's code where the array was supposed to be, and the "array" then holds machine code. The rule is to finish laying down data before defining anything else.

Thank you — anything else?