The Forth Language

VIOS is programmed in Forth. The resident Forth on the eZ80 is the shell, the compiler and the language of every application, module and GUI tool. This chapter teaches the language as VIOS implements it and documents where it differs from other Forths. If you already know Forth, read Cells and numbers, Malleability: redefinition and DEFER and Differences from ANS Forth first.

Every example in a forth block below runs as written, in order, at the vios> prompt (see The Console and REPL). Error transcripts are shown as plain text.

A first session

Forth reads a line as a sequence of words separated by spaces. Numbers go on the data stack; words take their arguments from the stack and leave results there. . prints and removes the top number.

2 3 + .
vios> 2 3 + .
5 ok

New words are defined with : and ;. A definition is compiled at once and can be used immediately, interactively or in later definitions:

: SQUARE ( n -- n*n ) DUP * ;
: CUBE ( n -- n^3 ) DUP SQUARE * ;
7 SQUARE .  3 CUBE .
vios> 7 SQUARE .  3 CUBE .
49 27 ok

The text in parentheses is a comment. By convention it states the word's stack effect: what it takes (left of --) and what it leaves (right). This manual writes every word's effect that way.

Typing at the console: a definition, a use, a loop
Typing at the console: a definition, a use, a loop

Cells and numbers

A cell is the unit the stacks and memory words work with. In VIOS a cell is 24 bits (3 bytes), the eZ80's native word size in ADL mode. The host build used for tests computes with the same 24-bit cells, masked from 32-bit C integers, so arithmetic is bit-for-bit identical on the Mac and the board.

Interpretation Range
Signed (., <, /) -8388608 .. 8388607
Unsigned (U., U<) 0 .. 16777215
Flag 0 is false; true is -1 (all 24 bits set, hex FFFFFF)
In memory 3 bytes, little-endian, no alignment

Arithmetic wraps modulo 2^24:

8388607 1+ .
-1 U.
8388607 1+ .
-8388608 ok
-1 U.
16777215 ok

Division truncates toward zero, as in C: / of -7 by 2 is -3, and MOD takes the sign of the dividend. Division by zero fails with arithmetic error. */ has no double-width intermediate: a b c */ is a b * c / in 24 bits.

-7 2 / .  7 -2 MOD .  100 3 4 */ .

Number syntax and BASE

A token that is not a word is parsed as a number in the current radix, held in the variable BASE (2..36, initially 10). Digits above 9 are letters; input is case-insensitive. Two prefixes override BASE for one number: $ for hexadecimal and % for binary. A sign comes first:

$FF .  %1010 .  -$10 .
HEX FF . 10 . DECIMAL
36 BASE ! ZZ . DECIMAL
$FF .  %1010 .  -$10 .
255 10 -16 ok
HEX FF . 10 . DECIMAL
FF 10 ok

Output from . and U. also uses BASE. A token whose value does not fit in 24 bits fails with number outside 24-bit range; a token that is neither a word nor a number fails with unknown word: NAME. $-10 is not a number (the sign must come before the prefix).

HEX and DECIMAL set BASE. Each background task has its own BASE (copied from its spawner when it starts), and module source is always compiled in decimal, so a HEX typed at the prompt never changes how a module loads.

The stacks

VIOS has three stacks, all of cells:

Stack Foreground depth In a background task Used by
Data stack 256 64 arguments and results
Return stack 256 64 return addresses, >R / R>
Loop stack 32 frames 16 frames DO loops (I, J)

The loop stack is separate from the return stack, unlike most Forths: I still works after >R, but EXIT from inside a loop must be preceded by UNLOOP. Overflowing or underflowing a stack is a recoverable error (data stack overflow, return stack underflow, and so on).

The common stack words:

Word Effect Word Effect
DUP ( x -- x x ) NIP ( a b -- b )
DROP ( x -- ) TUCK ( a b -- b a b )
SWAP ( a b -- b a ) ?DUP ( x -- 0 \| x x )
OVER ( a b -- a b a ) 2DUP ( a b -- a b a b )
ROT ( a b c -- b c a ) 2DROP ( a b -- )
-ROT ( a b c -- c a b ) 2SWAP ( a b c d -- c d a b )
DEPTH ( -- n ) 2OVER ( a b c d -- a b c d a b )

.S shows the stack without changing it, bottom first, with the depth in angle brackets:

1 2 3 ROT .S
DROP DROP DROP
1 2 3 ROT .S
<3> 2 3 1
ok

The return-stack words >R, R> and R@ park a value while you work below it. Use them only inside a definition and always move the value back before ;: the definition returns through the top of the return stack, so a leftover value is taken as a return address. On the host test build that fails cleanly with invalid memory; on the board it is a jump to whatever address the value names, which can hang or reset the machine. The same happens if you type them at the prompt.

: SUM3 ( a b c -- a+b+c ) >R + R> + ;
1 2 3 SUM3 .

When a command fails, VIOS clears all three stacks. Keep that in mind at the prompt: an error loses what you had on the stack.

Words and definitions

A word is a named dictionary entry. Names are 1 to 31 characters of any printable non-space characters and are case-insensitive: the reader converts every token to upper case before looking it up, so dup, Dup and DUP are the same word. (Text inside strings keeps its case.)

The dictionary holds up to 512 entries, of which about 250 are used by the system at boot. A colon definition is compiled into two code formats at once, token code and direct-threaded code (see Execution engines), and its source text is kept so that SEE can show it:

: GREET ( -- ) ." Hello from VIOS" CR ;
GREET
SEE GREET
SEE GREET
: GREET ." Hello from VIOS" CR ;
word GREET xt 255 kind 1 token 67020 direct 134830 token-bytes 14 direct-bytes 25
ok

The stored source is the canonical text the compiler read: upper case, single spaces, comments removed. The second line gives the word's execution token (xt), its kind (0 primitive, 1 colon, 2 constant, 3 variable, 4 deferred, 5 native), the addresses and sizes of both code forms (the numbers above come from the host build; on the board they are eZ80 addresses), and, for words loaded from a module, the module name and version.

A definition becomes visible only when ; completes it. Until then the name cannot be found, so a word cannot call itself by name; use RECURSE:

: FACT ( n -- n! ) DUP 1 > IF DUP 1- RECURSE * ELSE DROP 1 THEN ;
10 FACT .

If anything fails while a definition is being compiled (an unknown word, an unbalanced IF, a full dictionary), the half-built definition is discarded completely; nothing of it remains in the dictionary. Definitions may span several lines at the prompt: while one is open, the prompt reads compile>.

Definitions cannot nest (nested definition), and some words are refused inside them; the error names the reason (compile-only word, interactive tool inside definition).

How a line is read

For each token the reader tries, in this order:

  1. Outer commands such as LOAD, DIR, SAVE, MODULES (only when no definition is open). These parse their own arguments from the rest of the line; see Files and the SD Card and Modules and Persistence.
  2. Parser keywords: : ; IF ELSE THEN BEGIN UNTIL AGAIN WHILE REPEAT DO ?DO LOOP +LOOP LEAVE RECURSE EXIT IS ' ['] CHAR [CHAR] S" ." [ ] LITERAL IMMEDIATE POSTPONE DOES> and the tools WORDS .S SEE RESOURCES ENGINE TRACE LAST-ERROR ABORT SYNC (plus TASKS, TASK-OUT, TASK-SLICE and, on the board, BENCH). These are handled by the compiler itself. They are not dictionary entries: WORDS does not list them, ' cannot take their execution token, and defining a word with the same name has no effect on them. The control-structure and string keywords can still be used in your own compiling words through POSTPONE (see Extending the compiler).
  3. Dictionary words, newest definition first. Interactively the word runs; inside a definition a call to it is compiled, unless the word is immediate, in which case it runs at once, during compilation.
  4. Numbers, as described above.

Comments

( starts a comment that ends at the next ); \ starts a comment that ends at the end of the line. Both are recognised by the tokenizer whenever a token begins with them, so no space is needed after them:

1 ( one ) 2 (two) + . \ prints 3

Inside S" and ." text, ( and \ are ordinary characters. One consequence: the internal word (+LOOP) can never be typed, because the reader takes it for a comment.

Control structures

Control words compile branches, so they work only inside definitions; at the prompt they fail with compile-only word. Conditions are flags: zero is false, anything else is true.

IF ELSE THEN

: CLASSIFY ( n -- )
  DUP 0< IF ." negative" ELSE DUP 0= IF ." zero" ELSE ." positive" THEN THEN
  DROP CR ;
-5 CLASSIFY  0 CLASSIFY  12 CLASSIFY

Comparison words leave flags: =, <>, <, >, U<, U>, 0=, 0<, 0> and WITHIN (n lo hi WITHIN is true for lo <= n < hi). Combine flags with AND, OR, XOR; invert one with 0= (INVERT flips all bits, which is the same only for -1 and 0).

BEGIN loops

BEGIN ... flag UNTIL repeats until the flag is true. BEGIN ... flag WHILE ... REPEAT tests at the top. BEGIN ... AGAIN loops forever; leave it with EXIT.

: HALVES ( n -- ) BEGIN DUP WHILE DUP . 2 / REPEAT DROP CR ;
: POWERS ( -- ) 1 BEGIN DUP . 2* DUP 1000 > UNTIL DROP CR ;
20 HALVES POWERS

FIRST-SQUARE-OVER keeps its argument on the return stack while it searches and leaves through EXIT once the square is large enough (the R> DROP balances the >R before leaving):

: FIRST-SQUARE-OVER ( n -- m ) >R 1 BEGIN DUP DUP * R@ > IF R> DROP EXIT THEN 1+ AGAIN ;
50 FIRST-SQUARE-OVER .

The execution budget

A command typed at the prompt (each word executed interactively) may run at most 1,000,000 VM instructions. A runaway loop therefore ends with an error instead of hanging the console:

vios> : FOREVER BEGIN AGAIN ;
ok
vios> FOREVER
error: execution budget exhausted

A million instructions is plenty for interactive work: the loop below runs 300,000 iterations (about 900,000 instructions). Time spent inside native services, such as waiting in MS, does not count. Work that should run indefinitely belongs in a background task, which is scheduled in slices and has no total budget (see Tasks and Preemption). On the board you can also stop a running command with Ctrl+C.

: SUMTO ( n -- sum ) 0 SWAP 0 DO I + LOOP ;
300000 SUMTO .

(The sum is reduced modulo 2^24, as all arithmetic is.)

Counted loops: DO LOOP

limit start DO ... LOOP runs the body with the index going from start up to limit-1. I is the innermost index, J the next outer one.

: TABLE ( -- ) 4 1 DO 4 1 DO I J * . LOOP CR LOOP ;
TABLE

DO always runs the body at least once; with start equal to limit it would count all the way round 2^24 values. When the count can be zero, use ?DO, which skips the loop instead:

: STARS ( n -- ) 0 ?DO [CHAR] * EMIT LOOP CR ;
5 STARS  0 STARS

n +LOOP adds n to the index and stops when the index crosses the boundary between limit-1 and limit, in either direction, so negative steps count down:

: EVENS ( -- ) 10 0 DO I . 2 +LOOP CR ;
: COUNTDOWN ( -- ) 0 10 DO I . -3 +LOOP CR ;
EVENS COUNTDOWN

LEAVE exits the innermost loop at once and continues after its LOOP. To return from the whole definition from inside a loop, drop the loop frame first with UNLOOP, then EXIT:

: FIRST-DIVISOR ( n -- d ) DUP 2 ?DO DUP I MOD 0= IF DROP I UNLOOP EXIT THEN LOOP ;
91 FIRST-DIVISOR .  13 FIRST-DIVISOR .

Nesting is limited by the loop stack (32 active loops in the foreground, 16 in a task) and, at compile time, by the compiler's control stack (64 open structures) and 16 pending LEAVE/?DO exits per definition.

Variables, constants and data space

VARIABLE creates a word that pushes the address of a cell (initialised to 0). @ fetches a cell, ! stores one, +! adds to one and ? prints one. CONSTANT creates a word that pushes a value.

VARIABLE HITS
3 HITS !  2 HITS +!  HITS ?
100 CONSTANT LIMIT
LIMIT HITS @ - .

Variables, CREATEd buffers and all string literals live in data space, one 8192-byte arena. HERE is its next free address. CREATE makes a word that pushes the current HERE; then ALLOT reserves zeroed bytes, , appends a cell and C, a byte:

CREATE PRIMES 2 , 3 , 5 , 7 ,
PRIMES 3 CELLS + @ .
CREATE SCRATCH 32 ALLOT
SCRATCH 32 [CHAR] - FILL  SCRATCH 10 TYPE CR

CELLS converts a count of cells to bytes (n 3 *) and CELL+ adds 3. Bytes are read and written with C@ and C!; FILL, ERASE and MOVE work on ranges, and DUMP shows memory in hex:

S" VIOS" SCRATCH SWAP MOVE
SCRATCH 4 TYPE CR

Rules that differ from other Forths:

  • CREATE, ALLOT, HERE, ,, C,, VARIABLE, CONSTANT and DEFER are ordinary words, so they also work inside definitions: that is how you write your own defining words with DOES> (see Extending the compiler). Words that grow the dictionary are refused in background tasks.
  • ALLOT refuses negative sizes (negative ALLOT), and a request that does not fit fails with data space full. If that happens on the first ALLOT after a CREATE, the new word is removed again, so no name is left pointing past the end of data space.
  • No alignment is ever required; a cell can start at any byte address.

Memory addresses: board and host

On the board, addresses are real eZ80 addresses and @, !, C@, C! accept any of them. There is no memory protection: a store through a wrong address overwrites whatever is there, including the resident system. The host build used for testing only maps the dictionary regions (token code at hex 10000, direct code at hex 20000, data space at hex 40000) and reports invalid memory for anything else, which makes such bugs easy to find before they reach the board.

Strings and characters

S" reads text up to the next " on the same line and leaves its address and length; TYPE prints such a pair. ." prints text directly (it is S" ..." TYPE). One space after the opening word is skipped; everything else, including case, is kept:

S" Agon Light 2" TYPE CR
: BANNER ( -- ) ." VIOS " S" Forth" TYPE CR ;
BANNER
S" hello" NIP .

Every string literal is copied into data space with a trailing zero byte, both inside definitions and at the prompt. Typing ." hi" interactively therefore uses 3 bytes of data space each time; in a long session prefer CR and EMIT, or put repeated text in a word. A missing closing quote fails with unterminated string.

CHAR pushes the code of the first character of the next token at the prompt; inside definitions use [CHAR]. Both keep the character's case. EMIT prints one character, SPACE and SPACES print blanks, BL is 32, and COUNT unpacks a counted string (length byte first).

CHAR A .  CHAR a .
: VOWEL? ( c -- flag ) DUP [CHAR] A = OVER [CHAR] E = OR OVER [CHAR] I = OR
  OVER [CHAR] O = OR SWAP [CHAR] U = OR ;
CHAR E VOWEL? .  CHAR X VOWEL? .
CREATE MSG 3 C, CHAR Y C, CHAR E C, CHAR S C,
MSG COUNT TYPE CR

There is no ACCEPT, KEY? or pictured numeric output (<# # #>). KEY waits for one input byte; see The Console and REPL for where keys come from.

Execution tokens

Every dictionary entry has an execution token (xt): its index in the dictionary, starting at 1. ' pushes the xt of the next word at the prompt; ['] compiles one inside a definition. EXECUTE runs an xt.

' DUP .
: APPLY ( n xt -- ) EXECUTE . ;
5 ' DUP APPLY DROP
: TWICE ( xt -- ) DUP EXECUTE EXECUTE ;
: HI ( -- ) ." hi " ;
' HI TWICE CR

Execution tokens are also how background tasks are started (' WORD SPAWN) and how devices and GUI callbacks name their handlers.

Malleability: redefinition and DEFER

VIOS keeps the system malleable at run time: you can redefine any dictionary word, including primitives, at the prompt. The rule to remember is that a call is bound when it is compiled. Defining a new word with an old name affects only code compiled afterwards; words compiled earlier keep calling the definition that existed when they were compiled.

: TONE-NAME ( -- ) ." beep" ;
: ALERT ( -- ) TONE-NAME CR ;
: TONE-NAME ( -- ) ." chime" ;
ALERT
TONE-NAME CR
ALERT
beep
TONE-NAME CR
chime

The same holds for constants: their value is compiled into callers as a literal. Both definitions stay in the dictionary (WORDS lists both), and lookups find the newest one.

When callers should follow a change, use a deferred word. DEFER creates a binding slot; IS stores an xt in it; calling the deferred word runs whatever is bound now:

DEFER ALARM
: ALARM-TEXT ( -- ) ." ring ring" ;
' ALARM-TEXT IS ALARM
: WAKE-UP ( -- ) ALARM CR ;
WAKE-UP
: ALARM-QUIET ( -- ) ." (silent)" ;
' ALARM-QUIET IS ALARM
WAKE-UP

IS works interactively and inside definitions (['] WORD IS NAME). Calling a deferred word that was never bound fails with invalid execution token; IS on a word that is not deferred fails with IS requires deferred word. SEE on a deferred word shows kind 4.

Replaceable service bindings

The kernel defines six deferred words as conventional hooks for programs:

Binding Effect Bound at boot to
CONSOLE-EMIT ( c -- ) EMIT
CONSOLE-KEY ( -- c ) KEY
CLOCK-MS ( -- u ) MILLIS
FS-READ ( addr u dest cap -- n ior ) FILE-READ
FS-WRITE ( addr u src n -- ior ) FILE-WRITE
DISPLAY-WRITE ( addr u -- ) VTYPE

Write your programs against these names when you want their output, input, clock or storage to be replaceable later, by you or by a module, without recompiling them. The system's own words do not go through them: EMIT, TYPE and . always write to the console, and MS uses the board's tick. Rebinding CONSOLE-EMIT therefore cannot cut you off from the REPL.

VARIABLE EMITTED
: COUNTING-EMIT ( c -- ) 1 EMITTED +! EMIT ;
: REPORT ( -- ) S" logged" 0 DO DUP I + C@ CONSOLE-EMIT LOOP DROP CR ;
' COUNTING-EMIT IS CONSOLE-EMIT
REPORT  EMITTED ?
' EMIT IS CONSOLE-EMIT

A module may rebind a service while it loads, but it may not unbind one that was active before (cannot unbind an active service during module activation); see Modules and Persistence.

Extending the compiler

The compiler can be extended from Forth: you can write words that run while a definition is being compiled, build your own control structures, and define new defining words. These are the classic Forth tools; VIOS implements them on all three engines.

Immediate words. IMMEDIATE marks the word just defined as immediate: inside a definition it runs at once instead of being compiled. POSTPONE name compiles what name would do inside a definition, so an immediate word can lay down code. A new control word built on IF:

: UNLESS ( flag -- ) POSTPONE 0= POSTPONE IF ; IMMEDIATE
: CHECK ( n -- ) 0> UNLESS ." not positive " THEN ;
-5 CHECK  7 CHECK CR
not positive
Extending the compiler at the console: UNLESS, then a DOES> defining word
Extending the compiler at the console: UNLESS, then a DOES> defining word

SEE shows immediate at the end of an immediate word's metadata line. IMMEDIATE applies only to a word defined in the same unit (the same line or LOAD, or the same module); it cannot re-flag a module's words from the console.

Compile-time arithmetic. [ switches to interpreting inside a definition and ] switches back; LITERAL compiles the number on the stack. STATE holds true while compiling.

: SECONDS-PER-DAY [ 60 60 * 24 * ] LITERAL ;
SECONDS-PER-DAY .
86400

Defining words. Because CREATE, ALLOT and , now work inside definitions, DOES> can give the words a defining word makes their own behaviour: each child pushes its data address and runs the code after DOES>. >BODY gives a child's data address from its execution token.

: CONST ( x "name" -- ) CREATE , DOES> @ ;
42 CONST ANSWER  ANSWER .
: ARRAY ( n "name" -- ) CREATE CELLS ALLOT DOES> ( i -- addr ) SWAP CELLS + ;
10 ARRAY SCORES  77 3 SCORES !  3 SCORES @ .
' ANSWER >BODY @ .
42 77 42

Callers of a DOES> child inline its data address, so it runs about as fast as a dedicated "does" primitive would.

Text as code. EVALUATE ( addr u -- ) interprets a string as if typed, sharing the compiler state, so it also works as a text macro inside an immediate word. It nests up to 4 levels deep and takes at most 512 bytes of text per level.

S" 2 3 + ." EVALUATE
: SQ S" DUP *" EVALUATE ; IMMEDIATE
: CUBE3 DUP SQ * ;  4 CUBE3 .

Looking words up. FIND ( c-addr -- c-addr 0 | xt 1 | xt -1 ) looks up a counted string (1: immediate, -1: normal; parser keywords give 0). COMPILE, ( xt -- ) compiles a call to a word into the open definition.

Throwing code away. MARKER name makes a word that, when run, removes itself and everything defined after it, and gives back the dictionary entries, code, data and source-map space they used:

MARKER SCRATCH
: TEMP1 1 ; : TEMP2 2 ;
SCRATCH

A marker refuses to cut through module words, a DEFER still bound to a word it would remove, or code a live task might run. Use modules for code you keep.

Where they run. Words that grow the dictionary (CREATE, ,, ALLOT, COMPILE,, EVALUATE, markers ...) work at the prompt and in module source, but are refused in background tasks, in a module's test word and in observer hooks. Immediate words used in module source run on the C reference engine inside the module's transaction, so a module that fails to compile still leaves nothing behind.

Execution engines

The compiler emits every colon definition in two forms at once:

  • Token code: one-byte opcodes with three-byte operands. Compact (token arena 16 KiB).
  • Direct-threaded code: three-byte native addresses of primitive routines; each colon definition starts with a native CALL to the shared DOCOL entry, so a call to a colon word is one threaded cell (direct arena 32 KiB).

Three engines execute them, with identical semantics, stacks and error checks:

ENGINE name Runs Where
C token code, in the portable C reference interpreter board and host
TOKEN token code, in eZ80 assembly board only
DIRECT direct-threaded code, in eZ80 assembly board only (the board default)

ENGINE switches between them at any time, for example ENGINE TOKEN. Because every word exists in both forms, switching needs no recompilation. The name of the reference engine is C; ENGINE REFERENCE is rejected (engine unavailable). ENGINE is refused while background tasks are live. Module loading always compiles and tests in the reference engine and then returns to the engine you had.

ENGINE C
RESOURCES

On the board, BENCH times a set of micro-benchmarks (arithmetic, calls, branches, loops, memory) on all three engines with the 288 kHz timer and prints each sample, so you can compare them on your own hardware.

Inspecting the system

Tool Shows
WORDS every dictionary entry, oldest first
SEE name stored source and metadata of a word
.S the data stack
RESOURCES engine, dictionary and arena use, stack peaks
DUMP memory in hex (addr u DUMP)
LAST-ERROR the last runtime failure, with callers
TRACE name n single-step a word

RESOURCES prints one line (this one is from a fresh host build; on the board the engine is DIRECT and the numbers differ):

engine C words 289/512 token-bytes 1789 direct-bytes 4387 data-bytes 76 source-bytes 1711 map-entries 339/1792 data-peak 1 return-peak 0

words is used/maximum dictionary entries; the byte counts are used space in the token arena (16384 bytes), direct arena (32768), data space (8192) and source arena (8192); map-entries is the source map that TRACE and LAST-ERROR use (used/maximum); the peaks are the deepest data and return stacks since the last reset. The exact numbers depend on the build and on what your boot modules define.

DUMP prints each byte as two hex digits and starts a new line, headed by the address, at every 16-byte boundary, so a dump that starts in the middle of a 16-byte block has no address on its first line:

S" ABCDEFGH" DUMP

All of these tools are interactive only; inside a definition they fail with interactive tool inside definition.

Errors

When something goes wrong, the current command stops, the console prints error: and a message, and VIOS recovers:

  • all three stacks are cleared;
  • an unfinished definition is discarded;
  • the dictionary, data space, variables and BASE stay as they were.
vios> 1 2 3 FROB
error: unknown word: FROB
vios> .S
<0>
ok

The messages you will meet most:

Message Cause
unknown word: NAME not a word, keyword or number
data stack underflow / overflow too few arguments / more than 256 cells
return stack underflow / overflow unbalanced >R/R>, recursion deeper than 256
loop stack underflow / overflow I/J/UNLOOP outside a loop, more than 32 nested loops
invalid memory an address outside the mapped regions on the host build, or a write outside data space during a module transaction (on the board ordinary @/! are not checked)
arithmetic error division by zero, or -8388608 divided by -1
execution budget exhausted more than 1,000,000 instructions in one command
interrupted Ctrl+C on the board
number outside 24-bit range a literal that does not fit a cell
compile-only word IF, DO and friends outside a definition
unbalanced control flow ; with an open IF/BEGIN/DO, or THEN without IF
nested definition : inside a definition
use ['] in definitions / use [CHAR] in definitions ' or CHAR in a definition
invalid execution token EXECUTE of 0 or a bad xt, an unbound DEFER
dictionary full / code dictionary full / source dictionary full / data space full an arena is exhausted (see Limits)
missing name / token too long a defining word without a name, a token over 31 characters
unterminated string S" or ." without the closing quote

ABORT

ABORT clears the stacks, discards any unfinished definition and ignores the rest of the line (in a file being loaded, the rest of the file). It acts the moment the reader sees it, even inside a definition, so it cannot be compiled into a word as a run-time "abort with error". Use it at the prompt to start clean:

1 2 3 ABORT
DEPTH .
vios> 1 2 3 ABORT
ok
vios> DEPTH .
0 ok

LAST-ERROR

LAST-ERROR reports the most recent runtime failure of a foreground command, captured before the stacks were cleared:

vios> : BAD 1 0 / ;
ok
vios> : CALLER 5 BAD ;
ok
vios> CALLER
error: arithmetic error
vios> LAST-ERROR
last-error serial 2 engine DIRECT stale 0 base 10 message 61726974686D65746963206572726F72 ip 67017 word 424144 offset 8 src / ds-depth 3 rdepth 1 ldepth 0 ds 0 1 5
frame 0 word 43414C4C4552 offset 7 src BAD
ok

The fields are: a serial number of the failure; the engine; stale 1 if the dictionary has changed since (the source tokens may then be wrong); BASE; the message; the failing word, the code offset in it and the source token there (src /); the depths of the three stacks; the top 8 data cells, newest first; then one frame line per caller, innermost first (up to 16). The message and the word names are printed as hex-encoded bytes so the line is safe to parse: 61726974... is "arithmetic error", 424144 is "BAD", 43414C4C4552 is "CALLER". On the Mac, the CLI's :why command decodes it for you (see The Host Harness and MCP Tools). Failures inside background tasks are recorded per task instead (TASKS).

TRACE

TRACE name n runs a word one VM instruction at a time, for at most n steps (1 to 100000), and prints the state before each instruction:

: SQ2 ( n -- n*n ) DUP * ;
: SUMSQ ( a b -- a*a+b*b ) SQ2 SWAP SQ2 + ;
3 4 TRACE SUMSQ 20 .
step 1 word SUMSQ+0 src SQ2 depth 2 rdepth 0 top 4
step 2 word SQ2+0 src DUP depth 2 rdepth 1 top 4
step 3 word SQ2+1 src * depth 3 rdepth 1 top 4
step 4 word SQ2+2 src ; depth 2 rdepth 1 top 16
step 5 word SUMSQ+4 src SWAP depth 2 rdepth 0 top 16
...
step 11 word SUMSQ+10 src ; depth 1 rdepth 0 top 25
trace returned steps 11
25 ok

Each line shows the word and code offset about to run, the source token it was compiled from, the data and return stack depths and the top of the data stack. The offsets depend on the engine (token code offsets in the C and TOKEN engines, direct-code offsets in DIRECT). If the word fails, the trace ends with trace failed; if it runs out of steps, with trace step limit reached.

The source tokens come from a source map of 1792 entries shared by every word defined since boot. After many definitions in one session, words defined later trace with src - instead of their tokens; LAST-ERROR shows - the same way. A reboot (or a fresh boot that loads your modules) clears the map.

Differences from ANS Forth

VIOS Forth follows the classic Forth model but is not an ANS/Forth-2012 system. The main differences:

  • 24-bit cells, 3 bytes, no alignment. CELLS is 3 *.
  • Symmetric division: / and MOD truncate toward zero. */ has no double-cell intermediate. There are no double-cell words (M*, UM/MOD, D+ ...).
  • Keywords instead of immediate words: control structures and string words are parser keywords, not dictionary entries. FIND returns 0 for them and ' refuses them, but POSTPONE handles them, so : UNLESS POSTPONE 0= POSTPONE IF ; IMMEDIATE works. POSTPONE of a parsing keyword (S", .", [CHAR], ['], IS, POSTPONE) is refused.
  • No FORGET: use MARKER, or modules, to remove code. There is no CS-PICK/CS-ROLL; the control-flow stack is internal.
  • ' and CHAR are interpretation-only; use ['] and [CHAR] in definitions.
  • Separate loop stack: >R inside a DO loop does not hide I; still UNLOOP before EXIT from a loop.
  • ABORT is an interactive command, not a compilable word.
  • Errors clear the stacks and are reported as messages, not throw codes; there is no CATCH/THROW.
  • A per-command execution budget of 1,000,000 instructions in the foreground.
  • Calls are bound at compile time; use DEFER/IS for late binding.
  • Not provided: PICK, ROLL, 2@, 2!, CASE, VALUE/TO, K, ACCEPT, KEY?, .R, U.R, pictured numeric output, CMOVE, ALIGN, CHARS. Most are a one-line definition away, for example:
: CHARS ( n -- n ) ;
: 2@ ( addr -- x2 x1 ) DUP CELL+ @ SWAP @ ;
: 2! ( x2 x1 addr -- ) SWAP OVER ! CELL+ ! ;
CREATE PAIR 2 CELLS ALLOT
10 20 PAIR 2!  PAIR 2@ . .

Limits

The capacities that matter while programming (all from the code; the full list is in Appendix: Limits and Numbers):

Resource Limit Source
Dictionary entries 512 include/vios/config.h
Name length 31 characters forth/system.c
Name pool (all word names) 5120 bytes forth/system.c (VIOS_FORTH_NAME_BYTES)
EVALUATE nesting / text 4 levels, 512 bytes each forth/system.c (EVAL_POOL)
Token code arena 16384 bytes forth/system.c
Direct code arena 32768 bytes forth/system.c
Data space 8192 bytes forth/system.c
Source arena (for SEE) 8192 bytes forth/system.c
Data / return stack 256 / 256 cells include/forth/vm.h
Loop stack 32 frames include/forth/vm.h
Task data / return / loop stack 64 / 64 / 16 include/forth/system.h
Open control structures while compiling 64 forth/system.c
Pending LEAVE/?DO exits 16 per definition forth/system.c
Source-map entries (for TRACE/LAST-ERROR source tokens) 1792 forth/system.c
Native services 150 forth/system.c
Instructions per interactive command 1,000,000 forth/system.c
TRACE steps 1..100000 forth/system.c
REPL input line 255 characters forth/system.c

When the source-map pool is full, later definitions still compile and run; only their source tokens are missing from TRACE and LAST-ERROR (shown as -).