A Concatenative Language Derived from ColorForth
Pablo H. Reda
Repository: https://github.com/phreda4/r3
R3forth is a small, fast, concatenative programming language inspired by ColorForth. It compiles programs to native 64-bit code and is designed for direct OS interaction, real-time applications, and games.
If you've used Forth before, you'll feel at home quickly. If you haven't — don't worry. R3forth has a very small core that's easy to learn, and this tutorial will guide you step by step.
Note for Forth programmers: Unlike most Forth implementations, R3 compiles the entire program first and then executes it. Phrases like "pushes to stack" in this manual describe runtime behavior, not interactive REPL behavior.
- Programming a Computer
- The R3 Language
- Dictionary System
- Data Stack
- Arithmetic Operations
- Fixed Point Operations
- Conditionals
- Repetition
- Recursion
- Variables and Memory
- Text and Strings
- Registers A and B
- Return Stack
- Operating System Connection
- Libraries
- Complete Example: Simple Game
- Debugging Guide
- Common Patterns
- Performance Considerations
A computer needs a program to function — a precise description of what it should do.
This description is written in a language the machine understands. But the programming process begins earlier.
First comes an idea — for example, drawing a circle. Then we write code so the machine draws a circle with radius 1:
:draw 1 circle ;The computer first translates this code into machine code, the only language it truly understands. This process is called compilation.
Once compiled, the result is executed. If the code cannot be compiled, there is an error:
:draw 1 cicle ; | Error: 'cicle' not foundWhen we try to compile, the computer reports the error and executes nothing. Another possibility is that the program compiles correctly but doesn't do what we intended — a logic error.
To build a working program we need to:
- Have a problem or task to solve
- Have an idea of how to solve it
- Translate the idea into the programming language
- Compile without errors
- Execute and verify the result matches what we imagined
Important: We can only program what we understand well enough to describe. We cannot program what we don't know how it works.
Programming means building a recipe that describes behavior. This recipe is called source code or program.
A program has two kinds of definitions:
- DATA — also called memory, state, or variable
- CODE — also called order, routine, function, or action
As data we need to store numbers that represent:
- Quantities — for example: 3 lives
- Addresses or locations — for example: position 100 on the screen
- States — for example: jumping = 1, falling = 2
As code we build actions. Any behavior can be expressed with four elements:
- Sequence — one instruction follows the next
- Condition — an instruction runs only if a condition is met
- Repetition — an instruction repeats in some defined way
- Recursion — a word defined in terms of itself (least used)
The source code is a text file separated into words. A word is any sequence of letters, digits, or characters separated by spaces:
LIVES 134 -*jump*- livesCase is not significant — LIVES and lives are the same word.
Stack terms:
- TOS = Top Of Stack (the most recently pushed value)
- NOS = Next Of Stack (the second-to-last value)
- Stack cell = one storage slot (8 bytes / 64 bits)
Memory terms:
- byte = 8 bits —
c@/c!operations - word = 16 bits —
w@/w!operations - dword = 32 bits —
d@/d!operations - qword = 64 bits —
@/!operations (default)
Code terms:
- Word = a named function or data definition
- Definition = the code or data associated with a word
- Dictionary = the collection of all defined words
All operations work through a data stack using postfix notation: operands come first, then the operator.
5 3 + | Push 5, push 3, add → stack contains 8
10 dup * | Push 10, duplicate it, multiply → stack contains 100Important: Spaces define word boundaries. A very common source of errors is a missing space. The exact form of whitespace doesn't matter — one space, several spaces, or a newline are all equivalent.
- Parse source code word by word
- Prefixed words → interpret according to prefix
- Numbers → push to stack
- Known words → execute
- Unknown words → compilation error
R3 recognizes 8 prefixes. The first character of a word determines how it is interpreted:
| Prefix | Meaning | Example | Description |
|---|---|---|---|
| |
Comment | | This is a comment |
Ignored by the compiler, ends at line end |
^ |
Include | ^r3/lib/console.r3 |
Include code from the indicated file (entire line is the filename) |
" |
String | "Hello" |
Define a text string, ends with " |
: |
Action | :myword |
Define a code word |
# |
Data | #variable 5 |
Define data / variables |
$ |
Hexadecimal | $FF |
Hexadecimal number |
% |
Binary | %1010 |
Binary number |
' |
Address | 'word |
Address of a word |
Note: The address prefix
'requires a valid user-defined word. Base dictionary words do not have addressable locations.
The language starts with a predefined dictionary of around 200 words that represent basic computer operations (see the Reference document).
New words are added using the : and # prefixes. When the compiler searches for a word, it searches from the last defined to the first. You can redefine words — only the most recent definition is used.
Programming is creating words. A program is a dictionary that grows until you call the final word, which runs the whole thing.
The ^ prefix includes an external file and imports its exported definitions:
- Use
::instead of:to export a code word (visible when the file is included). - Use
##instead of#to export a data word. - All other definitions in the file remain private to that file.
This gives you a clean module system: only the API you intend to share becomes visible.
| Comments and documentation
^r3/lib/needed-library.r3
| Data definitions
#global-var1 0
#global-var2 * 100
| Helper words (private to this file)
:utility-word1 ... ;
:utility-word2 ... ;
| Exported words (accessible when included)
::public-word1 ... ;
::public-word2 ... ;
| Program entry point (always the last line)
: main-word ;Words must be defined before they are used:
| ✗ WRONG — word2 is not yet defined
:word1
word2 ;
:word2
"hello" ;
| ✓ CORRECT — define word2 first
:word2
"hello" ;
:word1
word2 ;#side 5
:square dup * ;
: side square ;Line 1 defines a variable with value 5.
Line 2 defines square: duplicates the top of stack and multiplies the two copies.
Line 3 is the program entry point — it pushes 5 (the value of side), then calls square.
At the end, the stack contains 25.
The semicolon ; ends execution of a word and returns to the caller. This means:
- A word can have multiple exit points (multiple semicolons)
- The semicolon is about control flow, not just marking the end of a definition
:example | n -- result
0? ( ; ) | First exit: return 0 if input is 0
1 =? ( ; ) | Second exit: return 1 if input is 1
dup * ; | Default exit: return n squaredA word definition without a closing ; falls through into the next definition. This is a deliberate feature:
:word1
something
:word2 | no ; here — word1 continues directly into word2
something-more ;
word1 | executes: something, something-more, end
word2 | executes: something-more, endThis avoids the overhead of an extra call when one word always leads to another. Use it intentionally and document it with a comment like | falls through to word2.
The data stack is R3forth's primary workspace. Think of it as a stack of plates: you can only add or remove from the top. The last value you put in is the first to come out — this is called LIFO (Last In, First Out).
All operations read their inputs from the stack and write their results back to it.
When numbers appear in source code, they are pushed onto the stack in order.
Each word can take and/or leave values on the data stack. A comment describes the stack state before and after the word, separated by --:
Format: | before -- after
:square | n -- n²
dup * ;
:distance | x1 y1 x2 y2 -- dist
rot - square | x1 x2 dy²
-rot - square | dy² dx²
+ sqrt ; | distance| Word | Stack Effect | Description |
|---|---|---|
DUP |
a -- a a |
Duplicate top of stack |
SWAP |
a b -- b a |
Exchange top two items |
DROP |
a -- |
Remove top of stack |
ROT |
a b c -- b c a |
Rotate three items |
-ROT |
a b c -- c a b |
Rotate three items (inverse) |
OVER |
a b -- a b a |
Copy second to top |
NIP |
a b -- b |
Remove second item |
PICK2 |
a b c -- a b c a |
Copy third item to top |
PICK3 |
a b c d -- a b c d a |
Copy fourth item to top |
2DUP |
a b -- a b a b |
Duplicate top two items |
2DROP |
a b -- |
Remove top two items |
2SWAP |
a b c d -- c d a b |
Exchange top two pairs |
Step-by-step example — track the stack after each operation:
5 3 | Stack: 5 3 (NOS=5, TOS=3)
+ | Stack: 8 (TOS=8)
dup | Stack: 8 8 (NOS=8, TOS=8)
2 | Stack: 8 8 2 (TOS=2)
* | Stack: 8 16 (TOS=16)
swap | Stack: 16 8 (TOS=8)
- | Stack: 8 (TOS=8)Some words produce values (like DUP), others consume them (like DROP), and some do both.
Stack Balance: If your word makes the stack grow indefinitely or empties it unexpectedly, the program has an error. This often cannot be detected before execution.
- Within a word: the net stack effect must match the stack comment.
- Within a loop: each iteration must leave the stack at the same depth.
- Within conditionals: all branches must have the same stack effect.
| ✗ WRONG — branches leave different stack depths
:bad-example | n --
5 >? ( dup ) | Branch adds a value
drop ; | Fails if branch ran
| ✓ CORRECT — balanced branches
:good-example | n --
5 >? ( dup drop ) | Branch is net-zero
drop ; | Always worksThe stack is for passing values between words, not for storing data. If you need a real stack structure, build one:
#mystack * 800 | 100 cells
#mystack> 'mystack
::mypush | v --
mystack> !+ 'mystack> ! ;
::mypop | -- v
-8 'mystack> +! mystack> @ ;
::mydepth | -- d
mystack> 'mystack - 3 >> ; | divide by 8| Word | Stack Effect | Description |
|---|---|---|
+ |
a b -- c |
c = a + b |
- |
a b -- c |
c = a - b |
| Word | Stack Effect | Description |
|---|---|---|
* |
a b -- c |
c = a * b |
/ |
a b -- c |
c = a / b (integer division) |
MOD |
a b -- c |
c = a mod b (remainder) |
/MOD |
a b -- c d |
c = a/b, d = a mod b |
| Word | Stack Effect | Description |
|---|---|---|
<< |
a b -- c |
Left bit shift (c = a << b) |
>> |
a b -- c |
Right bit shift, signed (c = a >> b) |
>>> |
a b -- c |
Right bit shift, unsigned (c = a >>> b) |
5 2 << | 20 (5 × 4)
5 1 >> | 2 (5 / 2, signed)
-2 1 >> | -1 (sign bit preserved)
-1 1 >>> | 9223372036854775807 (sign bit cleared)| Word | Stack Effect | Description |
|---|---|---|
NEG |
a -- -a |
Negate |
ABS |
a -- |a| |
Absolute value |
SQRT |
a -- b |
Integer square root |
*/ |
a b c -- d |
d = a×b/c without intermediate overflow |
*/ is particularly useful for proportional scaling:
| Scale a value from 0–100 to 0–255
75 255 100 */ | Result: 191 (no overflow)| Word | Stack Effect | Example |
|---|---|---|
AND |
a b -- c |
$ff $55 AND → $55 |
NAND |
a b -- c |
$ff $1 NAND → $fe |
OR |
a b -- c |
$2 $1 OR → 3 |
XOR |
a b -- c |
$3 2 XOR → 1 |
NOT |
a -- b |
0 NOT → -1 (all bits set) |
R3forth uses 48.16 fixed-point format: 48 bits for the integer part and 16 bits for the fractional part, all stored in a standard 64-bit cell (the same cell used for integers and addresses).
The practical rule: multiply any real number by 65536 (2¹⁶) to get its internal representation.
Example: 3.5 in 48.16 format
Integer part: 3 × 65536 = 196608
Fractional: 0.5 × 65536 = 32768
Stored value: 229376
Example: 1.5
1.5 × 65536 = 98304
Since only 16 bits are fractional, the integer range is enormous (up to ~281 trillion), making overflow virtually impossible in practice.
The language recognizes decimal-point literals and converts them automatically:
1.5 | Stored as 98304 (1.5 × 65536)
3.14159 | Stored as 205887 (3.14159 × 65536)64-bit cell:
[sign bit][47 integer bits][16 fractional bits]
Addition and subtraction work exactly like integers. Multiplication and division need special words from r3/lib/math.r3:
^r3/lib/math.r3
*. | f f -- f multiply two fixed-point numbers
/. | f f -- f divide two fixed-point numbers
int. | f -- a convert to integer (discard fractional part)Angles are expressed in turns (not degrees or radians): 1.0 = full circle, 0.5 = 180°, 0.25 = 90°.
| Available from r3/lib/math.r3
cos | f -- cos(f)
sin | f -- sin(f)
tan | f -- tan(f)
sqrt. | f -- sqrt(f) fixed-point square root
ln. | x -- r natural logarithm
exp. | x -- r exponential
root. | base root -- r nth root| Angle examples (in turns)
0.0 sin | sin(0°) = 0.0
0.25 sin | sin(90°) = 1.0
0.5 sin | sin(180°) = 0.0
0.75 sin | sin(270°) = -1.0For other bit distributions, R3 provides:
| Word | Stack Effect | Description |
|---|---|---|
*>> |
a b c -- d |
d = (a×b)>>c without bit loss |
<</ |
a b c -- d |
d = (a<<c)/b without bit loss |
| For 24.8 format (8 fractional bits):
a b 8 *>> | multiply two 24.8 numbers
a b 8 <</ | divide two 24.8 numbersParentheses mark code blocks. They are words too, and must be separated by spaces:
( code block )A conditional word followed by a code block executes the block only when the condition is true.
There are two families with different stack behavior — understanding this distinction is essential.
Unary conditionals check TOS without consuming it. The value stays on the stack, available for the next comparison or for drop.
| Word | Stack Effect | Description |
|---|---|---|
0? |
a -- a |
True when a = 0 |
1? |
a -- a |
True when a ≠ 0 |
-? |
a -- a |
True when a < 0 |
+? |
a -- a |
True when a ≥ 0 |
Binary conditionals compare NOS (a) against TOS (b). They consume TOS and leave NOS on the stack, regardless of the result. Think of a b >? as asking "is a greater than b?" — just as you would write it in mathematics.
| Word | Stack Effect | Description |
|---|---|---|
=? |
a b -- a |
True if a = b |
<? |
a b -- a |
True if a < b |
<=? |
a b -- a |
True if a ≤ b |
>? |
a b -- a |
True if a > b |
>=? |
a b -- a |
True if a ≥ b |
<>? |
a b -- a |
True if a ≠ b |
AND? |
a b -- a |
True if a AND b ≠ 0 |
NAND? |
a b -- a |
True if a NAND b ≠ 0 |
IN? |
a b c -- a |
True if b ≤ a ≤ c (consumes b and c) |
3
4 >? ( "Greater than 4" .print ) | is 3 > 4? No — block skipped
4 <? ( "Less than 4" .print ) | is 3 < 4? Yes — block runs
dropBecause binary conditionals leave a (NOS) on the stack, you can chain multiple tests on the same value:
var
4 >? ( ... ) | is var > 4? consumes 4, leaves var
2 <? ( ... ) | is var < 2? consumes 2, leaves var
drop | clean up var| Stack before: 10 5 (NOS=10, TOS=5)
10 5 >? ( "10 > 5" .print )
| >? asks: is NOS > TOS? → is 10 > 5? → YES, block runs
| After test: 10 (TOS=5 was consumed, NOS=10 remains)
drop
| Contrast — asking "is 5 > 10?":
5 10 >? ( ... )
| Stack before: 5 10 (NOS=5, TOS=10)
| >? asks: is 5 > 10? → NO, block skipped
| After test: 5 (TOS=10 consumed, NOS=5 remains)
dropPractical rule:
a b >?asks "isagreater thanb?". The value that stays on the stack is alwaysa(NOS).
After any test, the original value is still on the stack. You must remove it explicitly:
x
0? ( "Zero!" .print )
drop | MUST explicitly remove x when doneChaining works because the value persists across tests:
x
0? ( "Zero" .print )
+? ( "Non-negative" .print )
-? ( "Negative" .print )
drop | clean up once at the end:min | a b -- min
over >? ( drop ; ) | If a > b: drop b, return a
nip ; | Otherwise: drop a, return bR3forth omits IF-ELSE deliberately. This is not a limitation — it's a design choice that encourages you to break logic into small, named words (a practice called factoring). The result is code that reads almost like English and is much easier to test and reuse.
Compare the two styles:
// Traditional approach
if (x > 5) {
action1();
} else {
action2();
}| R3 approach — factor the decision:
:handle-small | x --
"Small value" .print
some-action ;
:handle-large | x --
"Large value" .print
other-action ;
:handler | x --
5 >? ( handle-large ; )
handle-small ;To replicate IF-ELSE when absolutely necessary, use early exit:
| Instead of: condition ( A ) else ( B )
:conditional condition ( A ; ) B ;For sequential integers, use jump tables:
:a0 "action 0" .print ;
:a1 "action 1" .print ;
:a2 "action 2" .print ;
#list 'a0 'a1 'a2
:action | n --
3 << | multiply by 8 (cell size)
'list + @ ex ;
2 action | Prints "action 2"For non-sequential values, chain comparisons with early exit:
:classify | value -- string
5 <? ( "less than 5" ; )
6 =? ( "is 6" ; )
7 =? ( "is 7" ; )
111 <? ( "between 8 and 110" ; )
"111 or more" ;
15 classify .print drop | "between 8 and 110"ERROR 1: Forgetting to clean the stack
| ✗ WRONG — value still on stack at end
:bad | value --
5 >? ( "Greater" .print )
;
| ✓ CORRECT
:good | value --
5 >? ( "Greater" .print )
drop ;ERROR 2: Misreading which value is consumed
| Stack before: 10 5 (NOS=10, TOS=5)
10 5 >? ( "Yes" .print )
| After: 10 (TOS=5 consumed, NOS=10 kept)
| Many expect the result to be 5 — it's not!ERROR 3: Nested conditions without cleanup
| ✗ WRONG — extra value left on stack
x 5 >? ( 10 <? ( "Between" .print ) )
| leaves x on stack at the end
| ✓ CORRECT
x 5 >? ( 10 <? ( "Between" .print drop ; ) )
dropWhen a conditional is placed inside a code block, it becomes a loop. While the condition is true, the block repeats. When false, execution jumps to the word after ).
( condition-word body )1 ( 10 <?
dup "%d " .print
1 + ) drop
| Prints: 1 2 3 4 5 6 7 8 9When TOS reaches 10, <? is false and the loop ends. The counter is dropped after.
Unary conditionals (1?) don't consume the stack, making countdown loops faster:
10 ( 1? 1 - ) drop
| Counts from 10 down to 1Whether to include 0 in the loop depends on where you place your code:
| Code BEFORE decrement — includes 0 in the iteration
10 ( 1? dup process 1 - ) drop
| Code AFTER decrement — excludes 0
10 ( 1? 1 - dup process ) drop#table * 800 | 10 × 10 × 8 bytes
'table >a
0 ( 10 <? 1 +
0 ( 10 <? 1 +
a@+ "%d " .print
) drop
.cr
) dropNull-terminated strings:
"hello" ( c@+ 1?
use-each-character
) 2drop
| When the loop exits: address+1 and 0 are on stack, both droppedWith a count:
"hello" 5 ( 1? 1 -
swap c@+
use-each-character
swap
) 2dropLoop with early exit:
:find-zero | addr cnt -- addr|0
( 1? 1 -
over @ 0? ( 2drop 0 ; ) drop
swap 8 + swap
)
2drop 0 ;Loop with accumulator — three styles:
| Pure stack (hard to read with many values):
:sum-array | addr cnt -- sum
0 swap
( 1? 1 -
-rot swap @+ rot + rot
)
drop nip ;
| Register for accumulator (cleaner):
:sum-array | addr cnt -- sum
0 >a
( 1? 1 -
swap @+ a> + >a swap
) 2drop
a> ;
| Register for address (cleanest):
:sum-array | addr cnt -- sum
swap >a
0
( swap 1? 1 -
swap a@+ +
) drop ;"text" ( c@+ 1? | continue while not zero
13 <>? | AND not carriage return
10 <>? | AND not line feed
drop
) 2drop
| On exit: the terminating character and the address are on stackRecursion happens naturally: as soon as a word definition begins, the word can call itself.
:fibonacci | n -- f
2 <? ( 1 nip ; ) | Base case: fib(0) = fib(1) = 1
1 - dup | n-1, n-1
1 - fibonacci | n-1, fib(n-2)
swap fibonacci | fib(n-2), fib(n-1)
+ ; | fib(n)
:factorial | n -- n!
dup 1 <=? ( drop 1 ; ) | Base case: 0! = 1! = 1
dup 1 -
factorial
* ;
5 factorial | Result: 120Recursion rule: Always ensure the termination condition is correct and that the stack is balanced between the base case and the recursive case.
When a word is called as the last operation before ;, R3 turns the call into a jump instead of a real call — no stack growth occurs. Tail-recursive words become loops:
:loopback | n -- 0
0? ( ; ) | Base case
1 -
loopback ; | Tail call → compiled as jump
10 loopback | Counts down efficientlyUse recursion when the problem naturally decomposes into smaller identical sub-problems (trees, divide-and-conquer). Use loops for linear traversal and simple counting.
| Recursion: natural for trees
:tree-sum | node -- sum
dup 0? ( ; )
dup @ swap
8 + @ tree-sum +
swap
16 + @ tree-sum + ;
| Iteration: better for arrays
:array-sum | addr cnt -- sum
0 swap ( 1? 1 -
swap @+ rot + swap
) 2drop ;Missing base case:
| ✗ WRONG — infinite recursion
:bad-countdown | n --
dup "%d " .print
1 - bad-countdown ;
| ✓ CORRECT
:good-countdown | n --
dup 0? ( drop ; )
dup "%d " .print
1 - good-countdown ;Stack imbalance between cases:
| ✗ WRONG — base case leaves wrong number of values
:bad-fib | n -- result
2 <? ( 1 ; ) | Leaves n and 1 on stack
dup 1 - bad-fib + ;
| ✓ CORRECT
:good-fib | n -- result
2 <? ( 1 nip ; ) | Replaces n with 1
1 - dup
1 - good-fib
swap good-fib + ;Variables define named locations in memory. The actual address is assigned at compile time — you don't need to know it, just use the name.
#lives 3
#positionX #positionY
#map * $400 | 1 KB buffer
#list 3 1 4
#energy 1000Using $1000 as an example base address:
| Name | Address | Value |
|---|---|---|
| lives | $1000 | 3 |
| positionx | $1008 | 0 |
| positiony | $1010 | 0 |
| map | $1018 | 0 0 0 … 0 (1KB) |
| list | $1418 | 3 1 4 |
| energy | $1430 | 1000 |
#var | one 64-bit cell, value 0
#var 33 | one cell, value 33
#var 33 11 | two cells: 33 and 11 (16 bytes total)The * syntax inside a # definition is not the multiply operator. It reserves a zero-initialized block of the given number of bytes:
#buffer * 1024 | reserve 1024 bytes, all zeros
#image * $10000 | reserve 64KB, zero-initialized
#pad * 80 | reserve 80 bytesThe variable name becomes a pointer to the start of the block. Compare with listing values:
#data 1 2 3 | Three 64-bit cells with values 1, 2, 3 (24 bytes)
#data * 24 | 24 bytes of zeros — same size, different contentUse * whenever you need a buffer or array that you'll fill at runtime.
#data 33 11 [ 1 2 ] ( 3 4 )| Offset | Size | Value | Type |
|---|---|---|---|
| +0 | 8 bytes | 33 | qword (default) |
| +8 | 8 bytes | 11 | qword (default) |
| +16 | 4 bytes | 1 | dword (from [ ]) |
| +20 | 4 bytes | 2 | dword (from [ ]) |
| +24 | 1 byte | 3 | byte (from ( )) |
| +25 | 1 byte | 4 | byte (from ( )) |
#string "hola" "que" 0| Offset | Content | Bytes |
|---|---|---|
| +0 | 'h' 'o' 'l' 'a' 0 | 5 |
| +5 | 'q' 'u' 'e' 0 | 4 |
| +9 | 0 (qword) | 8 |
5 'var ! | Store 5 at the address of var
'var @ | Fetch value from var
var | Same as 'var @ — pushes the value
1 'var +! | Add 1 to var| Word | Stack Effect | Description |
|---|---|---|
! |
value address -- |
Store value at address |
@ |
address -- value |
Fetch value from address |
+! |
val address -- |
Add val to value at address |
!+ |
value address -- address+8 |
Store and advance address by 8 |
@+ |
address -- address+8 value |
Fetch and advance address by 8 |
Memory layout: Each default cell is 8 bytes (64 bits). Sequential cells are 8 bytes apart.
| Size | Fetch | Store | Fetch+ | Store+ | Increment |
|---|---|---|---|---|---|
| 8 bits | c@ |
c! |
c@+ |
c!+ |
+1 byte |
| 16 bits | w@ |
w! |
w@+ |
w!+ |
+2 bytes |
| 32 bits | d@ |
d! |
d@+ |
d!+ |
+4 bytes |
| 64 bits | @ |
! |
@+ |
!+ |
+8 bytes |
:listshow
'list
@+ "%d " .print | prints 3
@+ "%d " .print | prints 1
drop ;
'list 8 + @ | pushes 1 (second element)
1 'positionX +! | add 1 to positionX
listshow | prints 3 1The language exposes the start of free memory with MEM. Beyond that, three words manage a simple stack-based allocator:
| Word | Stack Effect | Description |
|---|---|---|
HERE |
-- addr |
Next free memory address |
MARK |
-- |
Save HERE (mark current position) |
EMPTY |
-- |
Restore HERE (release since last MARK) |
MARK | mark level 1
HERE 'buffer1 !
1024 'HERE +! | allocate 1KB
MARK | mark level 2
HERE 'buffer2 !
2048 'HERE +! | allocate 2KB
process-with-buffer2
EMPTY | release buffer2
process-with-buffer1
EMPTY | release buffer1The advantage of this scheme: no garbage collector is needed.
┌─────────────────────────────────────┐
│ CODE MEMORY │
│ Compiled word definitions │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ STRING CONSTANTS │
│ Strings defined inside : words │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ VARIABLE MEMORY │
│ # definitions, data strings, │
│ fixed-size buffers │
└─────────────────────────────────────┘
↑ HERE points here
┌─────────────────────────────────────┐
│ FREE MEMORY (Dynamic) │
│ Managed by MEM / HERE / MARK / │
│ EMPTY — grows upward │
└─────────────────────────────────────┘
Text is a sequence of bytes in memory. Strings in source code are enclosed in double quotes:
"example text"
" with leading and trailing spaces "
"Say ""HELLO"" to everyone" | embed quotes by doubling themWhen R3 encounters a string literal, it stores the bytes in memory, appends a zero byte (null terminator), and pushes the address of the first character onto the stack.
In code definitions (:):
:greet "Hello" .println ;The string is stored in the string constants area and the address is pushed each time the word runs.
In data definitions (#):
#greeting "Hello"The bytes are stored in variable memory, including the null terminator. 'greeting is the address.
:printascii | t --
( c@+ 1? "%d " .print ) 2drop ;
"AB" printascii | prints: 65 66The .print word processes a format string with % placeholders, consuming values from the stack:
| Format | Description | Example |
|---|---|---|
%d |
Decimal number | 255 "%d" → "255" |
%b |
Binary number | 5 "%b" → "101" |
%h |
Hexadecimal | 255 "%h" → "ff" |
%s |
String address | "hello" "%s" → "hello" |
%% |
Literal % | "%%" → "%" |
253 254 255 "%d %b %h" .print
| prints: 255 11111110 fcNote: The format string consumes values from the stack in right-to-left order matching the
%placeholders. Useduporoverif you need to keep values on the stack after printing.
| String length
:strlen | str -- str len
0 over ( c@+ 1? drop swap 1 + swap ) 2drop ;
| String copy
:strcpy | dest src --
( c@+ 1? over c! swap 1 + swap ) 2drop c! ;
| String compare (returns 0 if equal)
:strcmp | str1 str2 -- n
( c@+ 1?
rot c@+ rot - 1? ( rot 2drop ; )
drop swap
) 3drop 0 ;To create an array of string pointers:
#a "uno"
#b "dos"
#c "tres"
#lista 'a 'b 'c | array of addressesTo create a contiguous block of strings:
#lista "uno" "dos" "tres" | bytes packed together (no address table)Registers A and B are two fast auxiliary variables optimized for memory traversal. Unlike stack values, they persist between word calls.
Primary use: Load an address into A or B, then use
A@+/A!+to read or write sequentially. This is much cleaner than managing pointer arithmetic on the data stack.
| Word | Stack Effect | Description |
|---|---|---|
>A |
a -- |
Load register A |
A> |
-- a |
Push register A value |
A+ |
a -- |
Add to register A |
A@ |
-- a |
Fetch qword from address in A |
A! |
a -- |
Store qword at address in A |
A@+ |
-- a |
Fetch qword from A, then A += 8 |
A!+ |
a -- |
Store qword at A, then A += 8 |
cA@+ |
-- a |
Fetch byte from A, then A += 1 |
cA!+ |
a -- |
Store byte at A, then A += 1 |
dA@+ |
-- a |
Fetch dword from A, then A += 4 |
dA!+ |
a -- |
Store dword at A, then A += 4 |
Register B has identical operations: >B, B>, B+, B@, B!, B@+, B!+, etc.
#list1 * $ffff
#list2 * $ffff
#minimum
:search | --
a@+ minimum <? ( 'minimum ! ; ) drop ;
:find-minimums | --
'list1 >a
a@ 'minimum !
1000 ( 1? 1- search ) drop
'list2 >a
a@ 'minimum !
1000 ( 1? 1- search ) drop ;search uses the address in register A. The first loop uses list1, the second uses list2 — no need to pass the address through the stack.
#table * 800 | 10 × 10 × 8 bytes
'table >a
0 ( 10 <? 1 +
0 ( 10 <? 1 +
a@+ "%d " .print
) drop
.cr
) dropRegisters persist across word calls. When you must call another word that may use registers, save them first:
| Using the data stack:
:needs-registers | --
a> b> | save A and B
... use registers ...
>b >a ; | restore (note reversed order)
| Using the built-in save/restore pair (cleaner):
:with-saved-registers | --
ab[ | save A and B to return stack
... use registers freely ...
]ba ; | restore A and B✓ Good: linear memory traversal, accumulator in loops, temporary address storage within a single word.
✗ Avoid: long-term storage across many words (use variables instead), or anywhere it would make the code harder to follow without documentation.
There is a second stack that handles word calls — storing the return address used each time a ; executes.
| Word | Stack Effect | Description |
|---|---|---|
>R |
a -- |
Push to return stack |
R> |
-- a |
Pop from return stack |
R@ |
-- a |
Copy top of return stack (non-destructive) |
Use with care: The return stack is safe to use as temporary storage within a single word, as long as every
>Ris matched by anR>before any;(including conditional exits). Avoid using it across word boundaries — each word must leave the return stack exactly as it found it.
Temporary storage:
:example | a b c --
>r >r | save b and c
... work with a ...
r> r> ; | restore c and bIndex preservation in a loop:
:process-array | addr cnt --
( 1? >r
@+ process-value
r>
1 -
) 2drop ;Imbalanced return stack:
| ✗ WRONG
:bad-word | n --
>r
... code ...
; | forgot r> — returns to wrong address!
| ✓ CORRECT
:good-word | n --
>r
... code ...
r> drop ;Conditional imbalance:
| ✗ WRONG
:bad | n --
>r
condition? ( r> process ) | only pops in one branch!
;
| ✓ CORRECT
:good | n --
>r
condition? ( r> process ; )
r> drop ;R3forth connects to the operating system through dynamic libraries (.dll on Windows, .so on Linux). This lets you call any C-compatible function from R3 code.
The pattern is always the same:
- Load the library with
LOADLIB - Get the function address with
GETPROC - Call it with
SYS0–SYS10depending on the number of arguments
| Word | Stack Effect | Description |
|---|---|---|
LOADLIB |
"name" -- liba |
Load a dynamic library |
GETPROC |
liba "name" -- aa |
Get a function address |
SYS0 |
aa -- r |
Call function (0 parameters) |
SYS1 |
a aa -- r |
Call function (1 parameter) |
SYS2 |
a b aa -- r |
Call function (2 parameters) |
SYS3..10 |
... aa -- r |
Call function (3–10 parameters) |
"user32.dll" LOADLIB 'user32 !
user32 "MessageBoxA" GETPROC 'msgbox !
| MessageBoxA(NULL, text, title, MB_OK)
0 "Hello" "Title" 0 msgbox SYS4 dropThe simplest starting point — just include the console library:
^r3/lib/console.r3
: "hello world" .println ;| Windows DLL | R3 Library | Purpose |
|---|---|---|
| SDL2.dll | ^r3/lib/sdl2.r3 |
Graphics and window management |
| SDL2_image.dll | ^r3/lib/sdl2image.r3 |
Image loading (PNG, JPG) |
| SDL2_mixer.dll | ^r3/lib/sdl2mixer.r3 |
Audio playback |
| SDL2_net.dll | ^r3/lib/sdl2net.r3 |
Network communication |
| SDL2_ttf.dll | ^r3/lib/sdl2ttf.r3 |
TrueType font rendering |
Platform support: R3 is designed for Windows, Linux, macOS(future), and Raspberry Pi(future), though these ports are in progress.
| Word | Stack Effect | Description |
|---|---|---|
::msec |
-- msec |
Milliseconds since program start |
::time |
-- hms |
Current time (hours, minutes, seconds packed) |
::date |
-- ymd |
Current date (year, month, day packed) |
:.time | --
time
dup 16 >> $ff and "%d:" .print | hour
dup 8 >> $ff and "%d:" .print | minute
$ff and "%d" .print | second
;| Word | Stack Effect | Description |
|---|---|---|
::.cls |
-- |
Clear console |
::.write |
"text" -- |
Write text |
::.print |
.. "fmt" -- |
Formatted output |
::.println |
"text" -- |
Write text + newline |
::.home |
-- |
Move cursor to top-left |
::.at |
x y -- |
Position cursor |
::.fc |
color -- |
Set foreground color |
::.bc |
color -- |
Set background color |
::.input |
-- |
Read line of text from keyboard |
::.inkey |
-- key |
Return pressed key (0 = none) |
::.cr |
-- |
Newline |
After
.input, the entered text is in the##padvariable.
| Word | Stack Effect | Description |
|---|---|---|
::rerand |
s1 s2 -- |
Initialize generator with two seeds |
::rand |
-- rand |
64-bit random number |
::randmax |
max -- value |
Random number in [0, max) |
time msec rerand | seed with time
100 randmax | random integer 0–99
5.0 randmax 5.0 - | random fixed-point -5.0 to 0.0| Word | Stack Effect | Description |
|---|---|---|
::SDLinit |
"title" w h -- |
Open window |
::SDLfull |
-- |
Set fullscreen |
::SDLquit |
-- |
Close window |
::SDLcls |
color -- |
Clear screen |
::SDLredraw |
-- |
Flip buffers |
::SDLshow |
'word -- |
Run word every frame |
::exit |
-- |
Exit the show loop |
Input variables:
| Variable | Description |
|---|---|
##SDLkey |
Pressed key code (0 = none) |
##SDLchar |
Character code |
##SDLx, ##SDLy |
Mouse position |
##SDLb |
Mouse button state |
| Word | Stack Effect | Description |
|---|---|---|
::SDLColor |
col -- |
Set color ($RRGGBB) |
::SDLPoint |
x y -- |
Draw pixel |
::SDLLine |
x1 y1 x2 y2 -- |
Draw line |
::SDLFRect |
x y w h -- |
Filled rectangle |
::SDLRect |
x y w h -- |
Rectangle outline |
::SDLFCircle |
r x y -- |
Filled circle |
::SDLCircle |
r x y -- |
Circle outline |
::SDLTriangle |
x1 y1 x2 y2 x3 y3 -- |
Filled triangle |
Images:
| Word | Stack Effect | Description |
|---|---|---|
::SDLImage |
x y img -- |
Draw image |
::SDLImages |
x y w h img -- |
Draw image scaled |
::SDLspriteZ |
x y zoom img -- |
Draw with zoom |
::SDLSpriteR |
x y ang img -- |
Draw with rotation |
Sprite sheets:
| Word | Stack Effect | Description |
|---|---|---|
::ssload |
w h file -- ss |
Load sprite sheet |
::ssprite |
x y n ss -- |
Draw sprite N centered |
::sspriter |
x y ang n ss -- |
Draw with rotation |
::sspritez |
x y zoom n ss -- |
Draw with scale |
^r3/lib/sdl2gfx.r3
^r3/lib/rand.r3
#sprites
#x 320.0 #y 240.0
#vx 0.0 #vy 0.0
:player
x int. y int. 2.0 0 sprites sspritez
vx 'x +! vy 'y +! ;
:game-update
SDLkey
>esc< =? ( exit )
<le> =? ( -2.0 'vx ! )
<ri> =? ( 2.0 'vx ! )
<up> =? ( -2.0 'vy ! )
<dn> =? ( 2.0 'vy ! )
>le< =? ( 0.0 'vx ! )
>ri< =? ( 0.0 'vx ! )
>up< =? ( 0.0 'vy ! )
>dn< =? ( 0.0 'vy ! )
drop ;
:game-draw
0 SDLcls
player
SDLredraw ;
:game-loop
game-update
game-draw ;
:main
"R3forth Game Demo" 640 480 SDLinit
time msec rerand
16 16 "player.png" ssload 'sprites !
'game-loop SDLshow
SDLquit ;
: main ;Note: Debugging tools are still under development.
| Error | Meaning | Solution |
|---|---|---|
Error: 'word' not found |
Misspelled or undefined | Check spelling; define before use |
Print a value without consuming it:
:debug-print | value -- value
dup "DEBUG: %d" .print .cr ;
5 3 + debug-print | Shows "DEBUG: 8", leaves 8 on stackShow the top 3 stack values:
:s3 | a b c -- a b c
".s: " .print
pick2 "%d " .print
over "%d " .print
dup "%d" .print .cr ;
1 2 3 s3 | .s: 1 2 3Trace execution:
:trace | "msg" --
"TRACE: " .write .println ;
:suspicious-word | n --
"Entering" trace
dup 0? ( "Found zero" trace drop ; )
"Processing" trace
process-value ;Missing DROP after conditional:
| Symptom: stack grows unexpectedly
| ✗ n still on stack at end
:buggy | n --
5 >? ( "Greater" .print )
;
| ✓
:fixed | n --
5 >? ( "Greater" .print )
drop ;Loop stack imbalance:
| Symptom: crash or freeze
| ✗ each iteration adds a value
:buggy-loop | --
10 ( 1? 1 -
dup process
) ;
| ✓
:fixed-loop | --
10 ( 1? 1 -
dup process drop
) drop ;Register collision:
| ✗ process-items may use register A
:outer | addr --
>a
process-items
a> @ ;
| ✓ pass on stack or save registers
:outer | addr --
ab[
>a
process-items
]ba
@ ;- Isolate the problem — comment out code until the error disappears
- Check stack balance — verify each word matches its stack comment
- Add trace statements — print values at key points
- Test with simple inputs — use known values to verify logic
- Check boundary conditions — test with 0, negative, and large values
:clamp | value min max -- clamped
rot
over <? ( nip ; ) | value < min → return min
nip
over >? ( drop ; ) | value > max → return max
nip ;#cbuffer * 800 | 100 cells
#cwrite 'cbuffer
#cread 'cbuffer
#ccount 0
:cbuffer-write | value --
ccount 100 >=? ( 2drop ; ) drop
cwrite !+
dup 'cbuffer 800 + >=? ( drop 'cbuffer )
'cwrite !
1 'ccount +! ;
:cbuffer-read | -- value
ccount 0? ( ; ) drop
cread @+
dup 'cbuffer 800 + >=? ( drop 'cbuffer )
'cread !
-1 'ccount +! ;#state 0
:state0
player-hit? ( 1 'state ! ; ) drop
handle-state-0 ;
:state1
player-safe? ( 0 'state ! ; ) drop
handle-state-1 ;
#state-table 'state0 'state1
:update-state | --
state 3 << 'state-table + @ ex ;#str-buffer * 4096
#str-pos 'str-buffer
:str-reset | --
'str-buffer 'str-pos ! ;
:str-add | "text" --
( c@+ 1?
str-pos c!+
'str-pos !
) 2drop ;
:str-get | -- "result"
0 str-pos c!
str-buffer ;
str-reset
"Hello " str-add
"World" str-add
str-get .println | "Hello World"Stack operations are fastest. Keep frequently-used values on the stack rather than in variables.
| Fast — pure stack
:fast | a b -- result
dup * swap dup * + sqrt ;
| Slower — memory reads
:slow | --
vara @ dup *
varb @ dup * + sqrt ;| Slow — pointer managed on stack
:slow-loop | addr cnt --
( 1? 1 -
over @ process
swap 8 + swap
) 2drop ;
| Fast — pointer in register A
:fast-loop | addr cnt --
swap >a
( 1? 1 -
a@+ process
) drop ;Countdown loops are faster because 1? doesn't consume the counter:
| Faster
10 ( 1? 1 - process ) drop
| Slower — requires more stack work
0 ( 10 <? dup process 1 + ) dropExit as soon as the answer is known:
:find-value | addr cnt target -- addr|0
>r
( 1? 1 -
over @ r@ =? ( r> 3drop ; )
swap 8 + swap
) r> 3drop 0 ;Factor for clarity, but inline trivial operations in tight loops:
| Overhead: tiny-helper is called 1000 times
:main-loop | --
1000 ( 1? 1 - dup tiny-helper process ) drop ;
| Better for tight loops: inline
:main-loop | --
1000 ( 1? 1 - dup 2 * process ) drop ;✓ DO:
- Write stack comments for every word
- Factor code into small, reusable words
- Use countdown loops (faster than count-up)
- Keep frequently-used values on the stack
- Test with boundary conditions (0, negative, large)
- Balance the stack in all code paths
✗ DON'T:
- Leave the stack imbalanced — every word should consume exactly what its stack comment promises
- Assume registers persist across calls to other words (save them if needed)
- Use deep stack operations — factor instead
- Mix memory access sizes carelessly
- Forget to
dropafter conditionals
R3forth Tutorial — see the companion Reference document for the complete base dictionary.