Learn Forth

The stack machine: RPN, words, the dictionary, and building a language up from almost nothing.

The Stack Machine: RPN, Words, and Your First Session

Install Gforth, push numbers onto the data stack, and call words in Reverse Polish Notation.

The Stack Machine: RPN, Words, and Your First Session

Forth is a tiny, stack-oriented, concatenative language and interactive environment created by Charles "Chuck" Moore around 1970 to control a radio telescope on a minicomputer with only a few kilobytes of memory. It is unlike almost every language you have met: there are no expressions, no operator precedence, no variables-by-default, no type system, and no garbage collector. A Forth program is just a stream of words separated by spaces, and those words push and pop values on a shared data stack. This track takes you from that first idea all the way down to Forth's raw, fully manual memory model and its defining trick: extending the compiler itself.

Getting a Forth

The classic way to learn Forth is interactively. Gforth, the GNU implementation, is standards-conforming and runs everywhere; install it from your package manager (apt install gforth, brew install gforth) and launch the REPL:

$ gforth
Gforth 0.7.3, Copyright (C) ...
Type `bye' to exit

You type words, press Enter, and Forth interprets the whole line, printing ok when it finishes. The word bye exits. Source files conventionally end in .fs, .fth, or .4th and are run with gforth file.fs.

Reverse Polish Notation

Forth uses Reverse Polish Notation (RPN, or postfix): operands come first, then the operator. There are no parentheses and no precedence because none are needed. To compute 2 + 3 you write the operands first, then the operator:

2 3 +    \ push 2, push 3, then + pops both and pushes 5

Reading it as a stack: 2 pushes 2, 3 pushes 3 (stack is now 2 3), and + pops the top two, adds them, and pushes 5. The word . ("dot") pops the top of the stack and prints it:

2 3 + .     \ prints: 5
10 4 - .    \ prints: 6   (10 - 4)
6 7 * .     \ prints: 42
20 5 / .    \ prints: 4   (integer division)

Notice that - and / are not commutative: 10 4 - means "10 minus 4". The operand that was pushed first is the left-hand side. A compound expression nests naturally with no brackets:

3 4 + 5 * .   \ (3 + 4) * 5 = 35

Here 3 4 + leaves 7 on the stack, then 5 * multiplies it by 5. The stack carries intermediate results, which is why Forth needs no parentheses at all.

Everything is a word

In Forth, the unit of code is the word. +, ., *, dup, bye are all words. The interpreter reads a word, looks it up in the dictionary (covered in the next lesson), and executes it. Numbers are the one exception: a token the dictionary does not recognize is parsed as a number and pushed. Comments come in two forms:

\ a backslash comment runs to end of line
( a parenthesized comment, often used for stack effects )

By convention, words are documented with a stack-effect comment showing what they consume and produce, written ( before -- after ) with the top of the stack on the right:

( + : n1 n2 -- sum )
( . : n -- )          \ consumes one number, prints it, leaves nothing

This notation is the heart of how Forth programmers reason: you track what is on the stack, not what is in named variables. Forth is case-insensitive in the standard, though many people write core words uppercase (DUP, SWAP) and their own words lowercase. Gforth accepts either; this track uses lowercase for readability and uppercase for the canonical primitives.

Why it is built this way

Forth's whole design follows from one constraint: it had to fit, compiler and all, in a machine with almost no memory. A stack removes the need for an expression parser and a register allocator. A dictionary of words removes the need for a separate linker. The result is a language you can implement in a few kilobytes and grow from the inside - which is exactly what the rest of this track does.

Reference

Next: the data stack itself - the stack-shuffling words that are Forth's "variables".

The Data Stack and Stack Shuffling

DUP, DROP, SWAP, OVER, ROT, the return stack, and thinking without named variables.

The Data Stack and Stack Shuffling

Forth has two stacks, both made of fixed-size machine words called cells (typically 64 bits on a modern desktop). The data stack (also called the parameter stack) holds operands and results - it is where almost all the action happens. The return stack holds return addresses and, when you ask, a few temporary values. Since Forth has no required local variables, your fluency comes from manipulating the data stack directly with a small vocabulary of stack-shuffling words.

The core stack operators

These words rearrange the top of the data stack. The stack-effect comment is the spec; top of stack is on the right:

DUP    ( a -- a a )         \ duplicate the top item
DROP   ( a -- )            \ discard the top item
SWAP   ( a b -- b a )      \ exchange the top two
OVER   ( a b -- a b a )    \ copy the second item to the top
ROT    ( a b c -- b c a )  \ rotate the third item to the top
NIP    ( a b -- b )        \ drop the second item
TUCK   ( a b -- b a b )    \ copy top under the second

Try them in the REPL. The word .s is invaluable: it prints the whole stack without consuming anything, with the top on the right:

1 2 3 .s        \ <3> 1 2 3  ok   (depth 3, top is 3)
DUP .s          \ <4> 1 2 3 3  ok
DROP DROP .s    \ <2> 1 2  ok
SWAP .s         \ <2> 2 1  ok

Why no parentheses means careful ordering

Because results live on the stack rather than in named variables, you constantly arrange operands into the order a word expects. Computing (a - b) when b is on top of a needs a SWAP:

\ stack has: 4 10  (10 on top), and we want 10 - 4
SWAP - .        \ -> 6

A more realistic example - squaring the top of stack - needs to duplicate it first because * consumes both copies:

DUP * .         \ n -> n*n ; e.g. 9 DUP * . prints 81

This is the Forth mindset: instead of x * x, you keep one value on the stack and DUP it. Deeply nested shuffles (ROT ROT, OVER OVER) are a sign you should factor the code into smaller words (next lesson) or use locals.

The return stack

The second stack normally holds return addresses for word calls, but Forth lets you borrow it for short-lived storage with three words. They must be balanced within a single word definition, and you must never leave the return stack unbalanced when a word returns, or you corrupt the call machinery:

>R     ( x -- ) ( R: -- x )   \ move top of data stack onto the return stack
R>     ( -- x ) ( R: x -- )   \ move it back
R@     ( -- x ) ( R: x -- x ) \ copy the top of the return stack

The ( R: ... ) part of the comment shows the return stack's effect. A typical use is parking a value out of the way while you work on others:

: example ( a b c -- result )
    >R          \ stash c on the return stack
    +           \ a + b
    R>          \ retrieve c
    * ;         \ (a+b) * c
3 4 5 example . \ (3+4)*5 = 35

The return stack gives you direct access to the call mechanism itself - a level of control most languages hide entirely. With great power comes the obligation to keep it balanced.

Locals: an escape hatch

Modern standard Forth offers local variables when stack juggling gets unreadable. They are declared with {: ... :} (Forth 2012) and scoped to the word:

: hypotenuse ( a b -- c )
    {: a b :}              \ name the two inputs
    a a *  b b *  + ;      \ a*a + b*b  (then you'd take a sqrt)

Locals are a convenience, not the Forth way; idiomatic code keeps definitions short enough that the stack stays manageable. But they exist for the cases where pure shuffling obscures intent.

Numbers, cells, and a typeless machine

Every stack item is a raw cell - an untyped machine word. The same 5 can be an integer, a boolean flag, an address, or a character code depending only on what word you apply next. Forth's canonical true flag is -1 (all bits set) and false is 0; comparison words like =, <, > produce these flags:

3 4 < .     \ -1  (true: 3 is less than 4)
5 5 = .     \ -1
2 9 > .     \ 0   (false)

This typelessness is liberating and dangerous in equal measure - there is nothing stopping you from adding an address to a character. The discipline is yours to keep, which becomes vital when we reach raw memory.

Reference

Next: defining your own words and growing the dictionary.

Defining Words, the Dictionary, and Control Flow

Use : and ; to compile new words into the dictionary, then branch and loop with them.

Defining Words, the Dictionary, and Control Flow

A Forth system is its dictionary: a linked list of every word it knows, each entry holding a name and the code (or data) behind it. The interpreter you have been using simply looks each token up in this dictionary. The single most important thing about Forth is that you extend the dictionary yourself - you build new words out of existing words, and from then on they are first-class members of the language, indistinguishable from the built-ins.

Defining a word with : and ;

The word : (colon) starts a new definition and switches the system from interpreting to compiling; ; (semicolon) ends it and switches back. Everything between is compiled into the dictionary under the given name:

: square ( n -- n*n )  DUP * ;

5 square .      \ 25
9 square .      \ 81

You just added square to the language. It is now a word like any other and can be used inside further definitions - this is the concatenative nature of Forth: programs are sequences of words composed from smaller words:

: cube ( n -- n^3 )  DUP square * ;     \ reuse square
3 cube .        \ 27

: sum-of-squares ( a b -- a^2+b^2 )
    square SWAP square + ;
3 4 sum-of-squares .    \ 25

Good Forth style is factoring: keep each word a few words long with a clear stack effect, then build up. A program ends up reading like a domain-specific vocabulary you defined for the problem.

How the dictionary works

When : runs, it carves a new entry into the dictionary's data space (the contiguous region we manage by hand in the next lesson), records the name, and links it to the previous entry. Compiling the body appends the addresses of the words you used. Because the dictionary is just a list searched newest-first, you can even redefine a word; later definitions shadow earlier ones, and words compiled before the redefinition keep calling the old version. The word words lists everything currently defined, and see decompiles a word in Gforth:

see square      \ : square  dup * ;

Constants and variables

Most Forth code lives on the stack, but for named storage there are two words. CONSTANT binds a name to a value (pushing it when called); VARIABLE reserves one cell of data space and pushes its address when called:

42 CONSTANT answer
answer .        \ 42

VARIABLE counter
0 counter !     \ store 0 at counter's address  ( ! is covered next lesson )
counter @ .     \ fetch and print: 0

Critically, VARIABLE returns an address, not a value - Forth makes the memory location explicit. That is your first glimpse of Forth's bare-metal memory model, which the next lesson explores fully.

Conditionals

Control flow uses words too, and it consumes a flag (0 = false, nonzero = true) from the stack. The structure is IF ... THEN, with optional ELSE. Note that THEN marks the end of the conditional (think "and then continue"):

: sign ( n -- )
    DUP 0> IF   ." positive"  DROP
    ELSE 0< IF  ." negative"
    ELSE        ." zero"
    THEN THEN ;
5 sign      \ positive
-3 sign     \ negative

." text" compiles a string that is printed when the word runs. Conditionals can only appear inside a definition (between : and ;), because they compile branch instructions.

Loops

Forth has counted loops and conditional loops. A counted loop runs DO ... LOOP with a limit and a start index on the stack; I pushes the current index:

: count-up ( n -- )
    0 DO  I .  LOOP ;      \ ( limit start DO ... )
5 count-up      \ 0 1 2 3 4

The order is limit start DO - 5 0 DO runs the index from 0 up to (but not including) 5. A conditional loop is BEGIN ... cond UNTIL (repeat until the flag is true) or BEGIN ... cond WHILE ... REPEAT:

: countdown ( n -- )
    BEGIN  DUP .  1-  DUP 0= UNTIL  DROP ;
3 countdown     \ 3 2 1

Like conditionals, all loop words must appear inside a definition.

The point of it all

By the end of a Forth program you have not written in a fixed language - you have grown a language up to meet your problem, word by word. The compiler is open: :/; are themselves just words, and as the final lesson shows, you can even teach the compiler new defining behaviors. First, though, the foundation that makes Forth a true systems language: raw memory.

Reference

Next: data space, HERE, ALLOT, and reading and writing raw memory with @ and !.

Raw Memory: HERE, ALLOT, @ and !

Forth's memory model - bump-allocate static data space and read/write addresses directly.

Raw Memory: HERE, ALLOT, @ and !

This is the lesson that places Forth among the systems languages. Forth has no garbage collector, no type system, and no bounds checking - memory is as raw and exposed as it gets. The static memory model is astonishingly simple: there is one contiguous region called data space, and a single pointer, HERE, that marks the next free byte. "Allocating" static memory means moving that pointer forward. Reading and writing is done with two words, @ and !, applied to bare addresses. Compared to C's malloc/free, this is one level lower.

The bump pointer: HERE and ALLOT

HERE ( -- addr ) pushes the address of the next unused cell of data space. ALLOT ( n -- ) reserves n bytes by advancing HERE past them. That is the entire static allocator - a bump pointer, exactly like an arena:

HERE          \ address of the next free byte (push it)
100 ALLOT     \ reserve 100 bytes; HERE is now 100 bytes higher
\ the address we pushed before ALLOT now names a 100-byte buffer

Because cells are a machine word wide, the standard gives you CELLS to convert a count of cells into bytes, and CELL+ to step one cell:

HERE 10 CELLS ALLOT CONSTANT buffer   \ reserve room for 10 cells, name the base
\ 'buffer' now pushes the base address of a 10-cell array

CREATE is the idiomatic way to name a region: it makes a dictionary word that, when called, pushes the address of the data space that follows it. , (comma) appends a cell of initialized data and bumps HERE; C, appends one byte:

CREATE primes  2 ,  3 ,  5 ,  7 ,  11 ,   \ five cells, initialized
\ 'primes' pushes the base address of the array

There is no deallocation of data space in the normal flow - it grows monotonically for the life of the program (you can roll HERE back with negative ALLOT, but that is rare and unstructured). Data space is effectively a permanent arena.

Reading and writing: @ (fetch) and ! (store)

Two words do all the work, and both operate on raw addresses with no checking whatsoever:

!    ( x addr -- )   \ STORE: write cell x to memory at addr
@    ( addr -- x )   \ FETCH: read the cell at addr onto the stack

Using a VARIABLE (which pushes its address) makes the pattern clear:

VARIABLE score
0 score !        \ store 0   into score's address
10 score +!      \ +! adds 10 to the cell in place
score @ .        \ fetch and print: 10

Byte-granular access uses C@ / C!, and +! ( n addr -- ) adds to a cell in place. Indexing into the array we created above is just address arithmetic - @ and ! plus CELLS:

CREATE data  5 CELLS ALLOT       \ uninitialized 5-cell array

42 data 2 CELLS + !              \ data[2] = 42
data 2 CELLS + @ .              \ -> 42

data 2 CELLS + computes base + 2*cell_size, the address of element 2, then ! stores and @ fetches. There is no bounds check: data 99 CELLS + ! will happily clobber whatever lives 99 cells away. Forth trusts you completely.

The optional heap: ALLOCATE / FREE / RESIZE

Data space is static and grow-only, so for genuinely dynamic memory standard Forth provides the optional Memory-Allocation word set - C-style malloc/free/realloc, GC-free and fully manual:

ALLOCATE   ( u -- addr ior )   \ request u bytes; ior = 0 on success
FREE       ( addr -- ior )     \ release a block from ALLOCATE/RESIZE
RESIZE     ( addr u -- addr' ior ) \ grow/shrink, possibly moving the block

Each returns an I/O result (ior), zero meaning success - you must check it. Every ALLOCATE must be paired with exactly one FREE, by hand, just like C:

: demo ( -- )
    100 ALLOCATE THROW    \ get 100 bytes; THROW aborts if ior <> 0
    ( addr )
    DUP 65 SWAP C!        \ write byte 'A' at the start
    DUP C@ EMIT           \ read it back and print: A
    FREE THROW ;          \ release it - forget this and you leak

THROW raises an exception when the ior is nonzero, which is the idiomatic way to handle allocation failure. The asymmetry of C's model is here too: forget the FREE and you leak; FREE twice or use the address afterward and you corrupt the heap. Forth gives you the mechanism and trusts you with the policy.

Forth's memory model in one picture

  • Static data: one arena, bump-allocated by HERE/ALLOT/,, named with CREATE/VARIABLE, grow-only, never individually freed.
  • Heap (optional): ALLOCATE/FREE/RESIZE, manual pairing, error codes you must check.
  • Access: @/! (cells), C@/C! (bytes), +! (in place) - raw addresses, no bounds checks, no types, no GC.

That is the whole model. There is no destructor, no defer, no smart pointer - nothing automatic at all. The next lesson sets this against the other six systems languages, all of which add some safety net that Forth deliberately omits.

Reference

Next: how the seven systems languages each manage memory, and where Forth sits.

Memory Models Across the Seven Systems Languages

Place Forth's raw HERE/ALLOT/@/! model next to C, C++, HolyC, Zig, Hare, and Odin.

Memory Models Across the Seven Systems Languages

Every language in this systems collection forgoes a garbage collector, but each draws the line between you and the machine in a different place. Forth sits at the extreme: rawer than C. This lesson lines up all seven so you can see precisely what Forth omits and why that is a deliberate design, not an oversight. The two ideas to watch are defer (schedule cleanup next to allocation so you cannot forget) and explicit allocators (make every allocation visible and swappable). Forth has neither; the newer languages have one or both.

C - pair every malloc with a free

C is the baseline. The heap is a global pool; you take from it with malloc/calloc/realloc and return with free, pairing them by hand. Raw pointers, no help, no checks:

int *a = malloc(n * sizeof(int));   // bytes, uninitialized
if (!a) return NULL;                 // can fail
/* ... use a ... */
free(a);                             // exactly one matching free

C++ - RAII and smart pointers

C++ keeps C's machine model but makes cleanup automatic through RAII: a destructor runs at scope exit. Idiomatic code never writes raw new/delete; it uses smart pointers that own and release for you:

#include <memory>
#include <vector>

auto p = std::make_unique<int>(42);   // freed automatically at scope end
std::vector<int> v{1, 2, 3};          // heap buffer; destructor frees it
// no delete anywhere; RAII handles it

std::unique_ptr is sole ownership, std::shared_ptr is reference-counted shared ownership. The compiler inserts the delete for you at the right place.

HolyC - manual MAlloc/Free in ring 0

HolyC is Terry Davis's TempleOS C dialect, JIT-compiled. Memory is manual like C but allocated from a per-task heap with MAlloc/Free (capitalized). It runs in ring 0 with no memory protection - a stray pointer can scribble over the whole OS:

U8 *buf = MAlloc(256);   // from the current task's heap
buf[0] = 'A';
Free(buf);               // Free(NULL) is allowed, like C's free

Zig - explicit allocators and defer

Zig's rule is no hidden allocations: any function that allocates takes an Allocator parameter, so allocation is always visible and swappable. Cleanup is scheduled with defer right next to the allocation:

const buf = try allocator.alloc(u8, 256);  // explicit allocator
defer allocator.free(buf);                  // runs at scope exit, guaranteed
// use buf...

errdefer runs only on the error path. The allocator is a value you pass around, so the same code can use a general-purpose, arena, or fixed-buffer allocator.

Hare - manual alloc/free with defer

Hare is a deliberately small "better C": manual alloc/free, a tiny runtime, no RAII, but it borrows defer so the cleanup sits beside the allocation:

let buf = alloc([0u8...], 256);  // allocate 256 zeroed bytes
defer free(buf);                  // scheduled cleanup
// use buf...

Slices ([]u8) bundle a pointer with a length, so the size travels with the data - a safety C lacks.

Odin - the implicit context allocator and defer

Odin routes allocation through an implicit context.allocator: new, make, free, and delete use whatever allocator the current context carries, and you can swap it (e.g. to an arena) for a whole scope. Cleanup again uses defer:

buf := make([]u8, 256)     // uses context.allocator
defer delete(buf)          // scheduled cleanup
// swap context.allocator to an arena to change all allocations in scope

This gives Zig-like swappability without threading an allocator through every signature.

Forth - raw HERE/ALLOT and @/! (and optional ALLOCATE/FREE)

Forth is the floor. Static memory is a bump-allocated arena: HERE is the pointer, ALLOT/, move it forward, CREATE/VARIABLE name regions - and it is grow-only, never individually freed. Dynamic memory is the optional, manual ALLOCATE/FREE set. Access is @/! on raw addresses with no types, no bounds checks, no GC, no defer, no destructors:

\ static arena allocation:
CREATE buf  256 ALLOT          \ bump HERE forward by 256 bytes
65 buf C!                      \ write 'A' to buf[0]  (no bounds check)
buf C@ EMIT                    \ read it back -> A

\ optional manual heap:
256 ALLOCATE THROW             \ like malloc; check the ior
DUP FREE THROW                 \ like free; pair it by hand

The spectrum at a glance

Language Free model Has defer? Allocator Safety net
C manual malloc/free no global heap none
C++ RAII / smart pointers no (uses destructors) global + custom destructors run automatically
HolyC manual MAlloc/Free no per-task heap none (ring 0)
Zig manual + defer yes (defer/errdefer) explicit parameter no hidden allocation
Hare manual + defer yes global, slices carry length slices bundle length
Odin manual + defer yes implicit context swappable per scope
Forth bump arena; optional ALLOCATE/FREE no the dictionary / arena none at all

Reading the table top to bottom is a tour from "automatic" (C++ destructors) through "explicit and deferred" (Zig, Hare, Odin) down to "raw and manual" (C, HolyC) and finally to Forth, which removes even the type system. Forth is not unsafe by accident - its smallness is the point. On a kilobyte-scale telescope controller there was no room for a GC, a type checker, or RAII, and Forth's answer was to hand you the bare mechanism and trust your discipline.

Reference

Next: Forth's most distinctive feature - extending the compiler itself with CREATE ... DOES>.

Extending the Compiler: CREATE ... DOES> and IMMEDIATE

Forth's signature trick - defining words that define words, so you grow the language itself.

Extending the Compiler: CREATE ... DOES> and IMMEDIATE

Forth's deepest, most distinctive idea is that the compiler is open and made of words you can extend. In most languages the compiler is a fixed black box; in Forth, :, ;, IF, and CREATE are themselves ordinary words, and you can write your own defining words that mint new words with custom behavior. This is how a few-kilobyte system grows up to meet any problem - you do not program in Forth so much as grow a new language out of it. This capstone lesson shows the mechanism.

Immediate words: running code at compile time

Normally, when Forth is compiling a definition (between : and ;), each word is compiled into the new word rather than executed. A word marked IMMEDIATE breaks that rule: it runs immediately, even during compilation. This is exactly how IF, LOOP, and ; themselves work - they are immediate words that emit branch instructions as the surrounding definition is built.

: greet  ." compiling now" ;  IMMEDIATE   \ contrived: prints while you compile callers

: test  greet  1 + ;     \ "compiling now" prints HERE, at compile time

Immediate words are how you add new control structures and syntax to Forth without touching the implementation. Combined with POSTPONE (which defers an immediate word's effect) you can build arbitrary compiler extensions - but the everyday tool for data-defining words is CREATE ... DOES>.

CREATE ... DOES> : words that define words

CREATE makes a new dictionary entry whose default behavior is to push the address of the data space after it. DOES> replaces that default behavior with whatever code follows, for every word the defining word creates. The split is the key:

  • The part before DOES> runs once, at definition time, to lay down data.
  • The part after DOES> runs every time a created word is invoked, with the data's address already on the stack.

The classic example is reinventing CONSTANT:

: my-constant ( n "name" -- )
    CREATE ,            \ at define time: store n into the new word's data space
    DOES> ( -- n )  @ ; \ at run time: fetch and push that stored value

42 my-constant answer   \ defines 'answer'
answer .                \ 42  -- DOES> code ran: pushed addr, @ fetched 42

my-constant is a word that defines words. Calling 42 my-constant answer runs the CREATE , part once (storing 42), and from then on answer runs the DOES> @ part. You have just taught Forth a new kind of definition.

A richer example: typed arrays

CREATE ... DOES> shines for building little data abstractions. Here is a defining word that creates fixed-size cell arrays which index themselves:

: array ( n "name" -- )
    CREATE  CELLS ALLOT          \ reserve n cells of data space
    DOES>   ( i -- addr )  SWAP CELLS + ;   \ index: base + i*cell

10 array scores         \ defines 'scores' as a 10-cell array

7 3 scores !            \ scores[3] = 7   ( value index array! )
3 scores @ .            \ -> 7

Every word array creates carries its own base address (captured by CREATE) and a shared indexing behavior (defined once in DOES>). This is Forth's answer to data structures: not a type system, but defining words that compose CREATE/ALLOT/@/! into reusable abstractions. You build the vocabulary your problem needs.

Why this is the heart of Forth

Most languages grow downward - you write your program in terms of a fixed set of primitives the language designers chose. Forth grows upward: :/; let you add ordinary words, IMMEDIATE/POSTPONE let you add syntax and control structures, and CREATE ... DOES> lets you add kinds of definitions. The compiler is not a wall between you and the language; it is more words in the same dictionary, open for extension. Chuck Moore's whole philosophy was to write the least code that solves the problem, and a self-extending compiler is what makes that possible: you bend the language to the task instead of bending the task to the language.

This reflectivity, paired with the raw memory model of the previous lessons, is what makes Forth simultaneously one of the smallest and one of the most malleable languages ever made - a complete interpreter, compiler, and application historically fitting in a few kilobytes, yet able to become a domain language for radio telescopes, boot firmware (Open Firmware), or your own project.

Where to go next

You have the full arc: the stack and RPN, stack shuffling and the return stack, defining words and the dictionary, the raw HERE/ALLOT/@/! memory model, how that model compares to the other six systems languages, and Forth's signature self-extension. The best next step is to open Gforth and build something small - a tiny calculator, a stack-based RPN interpreter (you are most of the way there), or a CREATE ... DOES> data structure - and watch the language grow under your hands.

Reference