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.

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:
- 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. - Parser keywords:
:;IFELSETHENBEGINUNTILAGAINWHILEREPEATDO?DOLOOP+LOOPLEAVERECURSEEXITIS'[']CHAR[CHAR]S"."[]LITERALIMMEDIATEPOSTPONEDOES>and the toolsWORDS.SSEERESOURCESENGINETRACELAST-ERRORABORTSYNC(plusTASKS,TASK-OUT,TASK-SLICEand, on the board,BENCH). These are handled by the compiler itself. They are not dictionary entries:WORDSdoes 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 throughPOSTPONE(see Extending the compiler). - 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.
- 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,CONSTANTandDEFERare ordinary words, so they also work inside definitions: that is how you write your own defining words withDOES>(see Extending the compiler). Words that grow the dictionary are refused in background tasks.ALLOTrefuses negative sizes (negative ALLOT), and a request that does not fit fails withdata space full. If that happens on the firstALLOTafter aCREATE, 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

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
CALLto the sharedDOCOLentry, 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.
CELLSis3 *. - Symmetric division:
/andMODtruncate 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.
FINDreturns 0 for them and'refuses them, butPOSTPONEhandles them, so: UNLESS POSTPONE 0= POSTPONE IF ; IMMEDIATEworks.POSTPONEof a parsing keyword (S",.",[CHAR],['],IS,POSTPONE) is refused. - No
FORGET: useMARKER, or modules, to remove code. There is noCS-PICK/CS-ROLL; the control-flow stack is internal. 'andCHARare interpretation-only; use[']and[CHAR]in definitions.- Separate loop stack:
>Rinside aDOloop does not hideI; stillUNLOOPbeforeEXITfrom a loop. ABORTis 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/ISfor 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
-).