R3forth

July 1, 2026 · View on GitHub

A Concatenative Language Derived from ColorForth

Pablo H. Reda

Repository: https://github.com/phreda4/r3


What is R3forth?

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.


Table of Contents

  1. Programming a Computer
  2. The R3 Language
  3. Dictionary System
  4. Data Stack
  5. Arithmetic Operations
  6. Fixed Point Operations
  7. Conditionals
  8. Repetition
  9. Recursion
  10. Variables and Memory
  11. Text and Strings
  12. Registers A and B
  13. Return Stack
  14. Operating System Connection
  15. Libraries
  16. Complete Example: Simple Game
  17. Debugging Guide
  18. Common Patterns
  19. Performance Considerations

Programming a Computer

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 found

When 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:

  1. Have a problem or task to solve
  2. Have an idea of how to solve it
  3. Translate the idea into the programming language
  4. Compile without errors
  5. 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.


The R3 Language

Introduction

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)

Core Concepts

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*-   lives

Case is not significant — LIVES and lives are the same word.

Essential Terminology

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

Stack-Based Execution

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 100

Word Boundaries

Important: 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.

Compilation Process

  1. Parse source code word by word
  2. Prefixed words → interpret according to prefix
  3. Numbers → push to stack
  4. Known words → execute
  5. Unknown words → compilation error

Prefix System

R3 recognizes 8 prefixes. The first character of a word determines how it is interpreted:

PrefixMeaningExampleDescription
|Comment| This is a commentIgnored by the compiler, ends at line end
^Include^r3/lib/console.r3Include code from the indicated file (entire line is the filename)
"String"Hello"Define a text string, ends with "
:Action:mywordDefine a code word
#Data#variable 5Define data / variables
$Hexadecimal$FFHexadecimal number
%Binary%1010Binary number
'Address'wordAddress of a word

Note: The address prefix ' requires a valid user-defined word. Base dictionary words do not have addressable locations.


Dictionary System

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.

Modules and Exports

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.

Program Structure

| 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 ;

Forward References Are Not Allowed

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 ;

Example Program

#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.

Understanding the Semicolon

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 squared

Fall-Through Definitions

A 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, end

This 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.


Data Stack

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.

Stack Notation

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

Stack Manipulation Words

WordStack EffectDescription
DUPa -- a aDuplicate top of stack
SWAPa b -- b aExchange top two items
DROPa --Remove top of stack
ROTa b c -- b c aRotate three items
-ROTa b c -- c a bRotate three items (inverse)
OVERa b -- a b aCopy second to top
NIPa b -- bRemove second item
PICK2a b c -- a b c aCopy third item to top
PICK3a b c d -- a b c d aCopy fourth item to top
2DUPa b -- a b a bDuplicate top two items
2DROPa b --Remove top two items
2SWAPa b c d -- c d a bExchange top two pairs

Stack Visualization

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 Rules

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.

  1. Within a word: the net stack effect must match the stack comment.
  2. Within a loop: each iteration must leave the stack at the same depth.
  3. 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 works

The 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

Arithmetic Operations

Addition and Subtraction

WordStack EffectDescription
+a b -- cc = a + b
-a b -- cc = a - b

Multiplication and Division

WordStack EffectDescription
*a b -- cc = a * b
/a b -- cc = a / b (integer division)
MODa b -- cc = a mod b (remainder)
/MODa b -- c dc = a/b, d = a mod b

Bit Shifting

WordStack EffectDescription
<<a b -- cLeft bit shift (c = a << b)
>>a b -- cRight bit shift, signed (c = a >> b)
>>>a b -- cRight bit shift, unsigned (c = a >>> b)

$\text{forth} 5 2 << | 20 (5 \times 4) 5 1 >> | 2 (5 / 2, \text{signed}) -2 1 >> | -1 (\text{sign} \text{bit} \text{preserved}) -1 1 >>> | 9223372036854775807 (\text{sign} \text{bit} \text{cleared}) $

Other Operations

WordStack EffectDescription
NEGa -- -aNegate
ABSa -- |a|Absolute value
SQRTa -- bInteger square root
*/a b c -- dd = 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)

Logical Operations

WordStack EffectExample
ANDa b -- c$ff \$55 AND → $55
NANDa b -- c$ff \$1 NAND → $fe
ORa b -- c$2 \$1 OR → 3
XORa b -- c$3 2 XOR → 1
NOTa -- b0 NOT → -1 (all bits set)

Fixed Point Operations

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)

Fixed Point Bit Layout

64-bit cell:
[sign bit][47 integer bits][16 fractional bits]

Basic Fixed Point Operations

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)

Trigonometric and Math Functions

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.0

Custom Fixed Point Formats

For other bit distributions, R3 provides:

WordStack EffectDescription
*>>a b c -- dd = (a×b)>>c without bit loss
<</a b c -- dd = (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 numbers

Conditionals

Parentheses 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.

Two Families of Conditionals

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.

WordStack EffectDescription
0?a -- aTrue when a = 0
1?a -- aTrue when a ≠ 0
-?a -- aTrue when a < 0
+?a -- aTrue 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.

WordStack EffectDescription
=?a b -- aTrue if a = b
<?a b -- aTrue if a < b
<=?a b -- aTrue if a ≤ b
>?a b -- aTrue if a > b
>=?a b -- aTrue if a ≥ b
<>?a b -- aTrue if a ≠ b
AND?a b -- aTrue if a AND b ≠ 0
NAND?a b -- aTrue if a NAND b ≠ 0
IN?a b c -- aTrue if b ≤ a ≤ c (consumes b and c)

Building Conditionals

3
4 >? ( "Greater than 4" .print )   | is 3 > 4? No — block skipped
4 <? ( "Less than 4" .print )      | is 3 < 4? Yes — block runs
drop

Because 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 Mechanics — Worked Example

| 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)
drop

Practical rule: a b >? asks "is a greater than b?". The value that stays on the stack is always a (NOS).

Value Persistence — The Critical Pattern

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 done

Chaining works because the value persists across tests:

x
0? ( "Zero"     .print )
+? ( "Non-negative" .print )
-? ( "Negative" .print )
drop    | clean up once at the end

Early Exit Pattern

:min | a b -- min
    over >? ( drop ; )  | If a > b: drop b, return a
    nip ;               | Otherwise: drop a, return b

No IF-ELSE — By Design

R3forth 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 ;

Switch-Case Alternatives

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"

Common Conditional Errors

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 ; ) )
drop

Repetition

When 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 )

Counting Up

1 ( 10 <?
    dup "%d " .print
    1 + ) drop
| Prints: 1 2 3 4 5 6 7 8 9

When TOS reaches 10, <? is false and the loop ends. The counter is dropped after.

Preferred Pattern: Countdown

Unary conditionals (1?) don't consume the stack, making countdown loops faster:

10 ( 1? 1 - ) drop
| Counts from 10 down to 1

Whether 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

Nested Loops

#table * 800  | 10 × 10 × 8 bytes

'table >a
0 ( 10 <? 1 +
    0 ( 10 <? 1 +
        a@+ "%d " .print
    ) drop
    .cr
) drop

Memory Traversal Patterns

Null-terminated strings:

"hello" ( c@+ 1?
    use-each-character
) 2drop
| When the loop exits: address+1 and 0 are on stack, both dropped

With a count:

"hello" 5 ( 1? 1 -
    swap c@+
    use-each-character
    swap
) 2drop

Loop 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 ;

Multiple Exit Conditions

"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 stack

Recursion

Recursion 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: 120

Recursion rule: Always ensure the termination condition is correct and that the stack is balanced between the base case and the recursive case.

Tail Call Optimization

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 efficiently

Recursion vs Iteration

Use 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 ;

Common Recursion Pitfalls

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 and Memory

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 1000

Using $1000 as an example base address:

NameAddressValue
lives$10003
positionx$10080
positiony$10100
map$10180 0 0 … 0 (1KB)
list$14183 1 4
energy$14301000

Defining Variables

#var           | one 64-bit cell, value 0
#var 33        | one cell, value 33
#var 33 11     | two cells: 33 and 11 (16 bytes total)

Memory Reservation with *

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 bytes

The 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 content

Use * whenever you need a buffer or array that you'll fill at runtime.

Mixed Data Types

#data 33 11 [ 1 2 ] ( 3 4 )
OffsetSizeValueType
+08 bytes33qword (default)
+88 bytes11qword (default)
+164 bytes1dword (from [ ])
+204 bytes2dword (from [ ])
+241 byte3byte (from ( ))
+251 byte4byte (from ( ))

String Definitions in Data

#string "hola" "que" 0
OffsetContentBytes
+0'h' 'o' 'l' 'a' 05
+5'q' 'u' 'e' 04
+90 (qword)8

Variable Access

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

Memory Access Words

WordStack EffectDescription
!value address --Store value at address
@address -- valueFetch value from address
+!val address --Add val to value at address
!+value address -- address+8Store and advance address by 8
@+address -- address+8 valueFetch and advance address by 8

Memory layout: Each default cell is 8 bytes (64 bits). Sequential cells are 8 bytes apart.

Memory Access Sizes

SizeFetchStoreFetch+Store+Increment
8 bitsc@c!c@+c!++1 byte
16 bitsw@w!w@+w!++2 bytes
32 bitsd@d!d@+d!++4 bytes
64 bits@!@+!++8 bytes

Example Usage

: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 1

Dynamic Memory

The language exposes the start of free memory with MEM. Beyond that, three words manage a simple stack-based allocator:

WordStack EffectDescription
HERE-- addrNext 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 buffer1

The advantage of this scheme: no garbage collector is needed.

Complete Memory Map

┌─────────────────────────────────────┐
│  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 and Strings

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 them

When 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.

Two Kinds of Strings

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.

Character-by-Character Processing

:printascii | t --
    ( c@+ 1? "%d " .print ) 2drop ;

"AB" printascii    | prints: 65 66

Formatted Output

The .print word processes a format string with % placeholders, consuming values from the stack:

FormatDescriptionExample
%dDecimal number255 "%d" → "255"
%bBinary number5 "%b" → "101"
%hHexadecimal255 "%h" → "ff"
%sString address"hello" "%s" → "hello"
%%Literal %"%%" → "%"
253 254 255 "%d %b %h" .print
| prints: 255 11111110 fc

Note: The format string consumes values from the stack in right-to-left order matching the % placeholders. Use dup or over if you need to keep values on the stack after printing.

Common String Operations

| String length
:strlen | str -- str len
    0 over ( c@+ 1? drop swap 1 + swap ) 2drop ;

| String copy
:strcpy | dest src --
    ( c@+ 1?
        swap >r swap c!+ r>
    ) nip swap c! ;

| String compare (returns 0 if equal)
| Note: only detects a difference if str1 ends before str2 (or the
| characters differ). If str2 is shorter than str1, this still reports
| equal at str2's terminator -- restructure with both bounds checked
| together if you need full-length comparison.
:strcmp | str1 str2 -- n
    ( c@+ 1?
        rot c@+ rot - 1? ( nip nip ; )
        drop swap
    ) 3drop 0 ;

String Arrays

To create an array of string pointers:

#a "uno"
#b "dos"
#c "tres"
#lista 'a 'b 'c     | array of addresses

To create a contiguous block of strings:

#lista "uno" "dos" "tres"   | bytes packed together (no address table)

Registers A and B

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.

Register Operations

WordStack EffectDescription
>Aa --Load register A
A>-- aPush register A value
A+a --Add to register A
A@-- aFetch qword from address in A
A!a --Store qword at address in A
A@+-- aFetch qword from A, then A += 8
A!+a --Store qword at A, then A += 8
cA@+-- aFetch byte from A, then A += 1
cA!+a --Store byte at A, then A += 1
dA@+-- aFetch 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.

Example: Finding a Minimum

#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.

Nested Loops with Registers

#table * 800    | 10 × 10 × 8 bytes

'table >a
0 ( 10 <? 1 +
    0 ( 10 <? 1 +
        a@+ "%d " .print
    ) drop
    .cr
) drop

Saving and Restoring Registers

Registers 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

When to Use Registers

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.


Return Stack

There is a second stack that handles word calls — storing the return address used each time a ; executes.

WordStack EffectDescription
>Ra --Push to return stack
R>-- aPop from return stack
R@-- aCopy 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 >R is matched by an R> before any ; (including conditional exits). Avoid using it across word boundaries — each word must leave the return stack exactly as it found it.

Safe Usage Patterns

Temporary storage:

:example | a b c --
    >r >r           | save b and c
    ... work with a ...
    r> r> ;         | restore c and b

Index preservation in a loop:

:process-array | addr cnt --
    ( 1? >r
        @+ process-value
        r>
        1 -
    ) 2drop ;

Common Errors

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 ;

Operating System Connection

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:

  1. Load the library with LOADLIB
  2. Get the function address with GETPROC
  3. Call it with SYS0SYS10 depending on the number of arguments
WordStack EffectDescription
LOADLIB"name" -- libaLoad a dynamic library
GETPROCliba "name" -- aaGet a function address
SYS0aa -- rCall function (0 parameters)
SYS1a aa -- rCall function (1 parameter)
SYS2a b aa -- rCall function (2 parameters)
SYS3..10... aa -- rCall function (3–10 parameters)

Example

"user32.dll" LOADLIB 'user32 !

user32 "MessageBoxA" GETPROC 'msgbox !

| MessageBoxA(NULL, text, title, MB_OK)
0 "Hello" "Title" 0 msgbox SYS4 drop

Console Access

The simplest starting point — just include the console library:

^r3/lib/console.r3

: "hello world" .println ;

Available Libraries

Windows DLLR3 LibraryPurpose
SDL2.dll^r3/lib/sdl2.r3Graphics and window management
SDL2_image.dll^r3/lib/sdl2image.r3Image loading (PNG, JPG)
SDL2_mixer.dll^r3/lib/sdl2mixer.r3Audio playback
SDL2_net.dll^r3/lib/sdl2net.r3Network communication
SDL2_ttf.dll^r3/lib/sdl2ttf.r3TrueType font rendering

Platform support: R3 is designed for Windows, Linux, macOS(future), and Raspberry Pi(future), though these ports are in progress.


Libraries

Core — ^r3/lib/core.r3

WordStack EffectDescription
::msec-- msecMilliseconds since program start
::time-- hmsCurrent time (hours, minutes, seconds packed)
::date-- ymdCurrent 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
    ;

Console — ^r3/lib/console.r3

WordStack EffectDescription
::.cls--Clear console
::.write"text" --Write text
::.print.. "fmt" --Formatted output
::.println"text" --Write text + newline
::.home--Move cursor to top-left
::.atx y --Position cursor
::.fccolor --Set foreground color
::.bccolor --Set background color
::.input--Read line of text from keyboard
::.inkey-- keyReturn pressed key (0 = none)
::.cr--Newline

After .input, the entered text is in the ##pad variable.

Random Numbers — ^r3/lib/rand.r3

WordStack EffectDescription
::rerands1 s2 --Initialize generator with two seeds
::rand-- rand64-bit random number
::randmaxmax -- valueRandom 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

Graphics — ^r3/lib/sdl2.r3

WordStack EffectDescription
::SDLinit"title" w h --Open window
::SDLfull--Set fullscreen
::SDLquit--Close window
::SDLclscolor --Clear screen
::SDLredraw--Flip buffers
::SDLshow'word --Run word every frame
::exit--Exit the show loop

Input variables:

VariableDescription
##SDLkeyPressed key code (0 = none)
##SDLcharCharacter code
##SDLx, ##SDLyMouse position
##SDLbMouse button state

Drawing — ^r3/lib/sdl2gfx.r3

WordStack EffectDescription
::SDLColorcol --Set color ($RRGGBB)
::SDLPointx y --Draw pixel
::SDLLinex1 y1 x2 y2 --Draw line
::SDLFRectx y w h --Filled rectangle
::SDLRectx y w h --Rectangle outline
::SDLFCircler x y --Filled circle
::SDLCircler x y --Circle outline
::SDLTrianglex1 y1 x2 y2 x3 y3 --Filled triangle

Images:

WordStack EffectDescription
::SDLImagex y img --Draw image
::SDLImagesx y w h img --Draw image scaled
::spriteZx y zoom img --Draw with zoom
::spriteRx y ang img --Draw with rotation

Sprite sheets:

WordStack EffectDescription
::ssloadw h file -- ssLoad sprite sheet
::sspritex y n ss --Draw sprite N centered
::sspriterx y ang n ss --Draw with rotation
::sspritezx y zoom n ss --Draw with scale

Complete Example: Simple Game

^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 ;

Debugging Guide

Note: Debugging tools are still under development.

Common Error Messages

ErrorMeaningSolution
Error: 'word' not foundMisspelled or undefinedCheck spelling; define before use

Debugging Techniques

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 stack

Show 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 3

Trace execution:

:trace | "msg" --
    "TRACE: " .write .println ;

:suspicious-word | n --
    "Entering" trace
    dup 0? ( "Found zero" trace drop ; )
    "Processing" trace
    process-value ;

Common Bug Patterns

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
    @ ;

Debugging Workflow

  1. Isolate the problem — comment out code until the error disappears
  2. Check stack balance — verify each word matches its stack comment
  3. Add trace statements — print values at key points
  4. Test with simple inputs — use known values to verify logic
  5. Check boundary conditions — test with 0, negative, and large values

Common Patterns

Bounded Value (Clamp)

:clamp | value min max -- clamped
    >r                        | value min           (max saved on return stack)
    over <? ( drop ; ) nip    | clamp to >= min
    r>                        | bring max back
    over >? ( drop ; ) nip ;  | clamp to <= max

Circular Buffer

#cbuffer * 800    | 100 cells
#cwrite 'cbuffer
#cread  'cbuffer
#ccount 0

:cbuffer-write | value --
    ccount 100 >=? ( 2drop ; ) drop
    cwrite !+
    'cbuffer 800 + >=? ( drop 'cbuffer )
    'cwrite !
    1 'ccount +! ;

:cbuffer-read | -- value
    ccount 0? ( ; ) drop
    cread @+                              | newaddr value
    swap                                  | value newaddr
    'cbuffer 800 + >=? ( drop 'cbuffer )  | value newaddr'
    'cread !
    -1 'ccount +! ;

State Machine

#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 ;

String Builder

#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"

Performance Considerations

Stack vs Memory

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 ;

Registers for Memory Traversal

| 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 vs Count-up

Countdown loops are faster because 1? doesn't consume the counter:

| Faster
10 ( 1? 1 - dup process ) drop

| Slower — requires more stack work
0 ( 10 <? dup process 1 + ) drop

Early Exit

Exit as soon as the answer is known:

:find-value | addr cnt target -- addr|0
    >r
    ( 1? 1 -
        over @ r@ =? ( r> 3drop ; )
        drop
        swap 8 + swap
    ) r> 3drop 0 ;

Factoring vs Inlining

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 ;

Best Practices

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 drop after conditionals

R3forth Tutorial — see the companion Reference document for the complete base dictionary.