Everything you type at the console lives in RAM and is gone at the next power cycle. To keep code, you put it in a module: a named, versioned piece of Forth source that VIOS compiles in a protected transaction, keeps verbatim, can roll back, saves to the SD card as a package and loads again at boot.
This chapter explains what a module is, how to build, test, activate and roll one back, how packages and the two boot slots work, and what is and is not kept across a restart.
What a module is
A module is exact ASCII source text, at most 4096 bytes, with:
| Field | Meaning |
|---|---|
| name | 1 to 31 printable characters, for example MATH |
| version | 1 to 31 printable characters, your choice (1, 1.2, 2026-10-21) |
| test word | optional: a word the source defines that must leave exactly one true flag |
| dependencies | up to 4 other modules, each pinned by the CRC-32 of its source |
| source CRC-32 | computed by VIOS; the module's identity |
VIOS keeps the source text itself, not only the compiled code. That is what
lets it show you the exact source later (MODULE-SOURCE,
the GUI Browser), save it as a package and rebuild the module at boot.
Limits of the module system:
| Limit | Value |
|---|---|
| Modules | 8 |
| Source per module | 4096 bytes, ASCII (tab, CR, LF and printable characters) |
| Source pool for all retained generations | 32768 bytes |
| Dependencies per module | 4 |
| Generations kept per module | 2 (current and previous) |
A module's source runs in a transaction with these rules, so loading a module can never leave a half-built dictionary behind:
- It starts in
DECIMAL, whatever your consoleBASEis. - It may define words, variables, constants and deferred words, and run
initialisation code, but it may not do I/O:
EMIT,.,CR,KEYand every device service fail with "I/O disabled during module transaction".SPAWNand the interactive tools (WORDS,SEE,TASKS, ...) are unavailable too. - It must end outside a definition and leave the data stack empty.
- It has a budget of 1,000,000 instructions.
- If a test word is given, it runs after the source and must return exactly one non-zero value. Whatever the test writes to memory is thrown away; only the effects of the source itself become live.
- It may not unbind a deferred service word that is currently in use.
If any rule is broken, or any word fails, the whole transaction is undone and the dictionary is exactly as before.
Building a module from the console
At the console, a module is built with a few commands. They are handled by the command interpreter, not compiled, so you type them on their own lines, never inside a colon definition:
| Command | Effect |
|---|---|
MODULE-BEGIN name version test |
start a new source in the builder (- = no test word) |
MODULE-DEP name crc |
pin a dependency: module name with source CRC crc (8 hex digits) |
MODULE-DATA hex |
append source bytes as hex digits (110 bytes per line fit every console path) |
MODULE-STAGE |
compile and test the source in a dry run, keep it pending |
MODULE-ACTIVATE name |
compile it for real and make it live |
MODULE-DISCARD |
throw away the pending module and the builder |
MODULE-ROLLBACK name |
undo the latest activation |
The source travels as hex so that any character, including spaces, line
breaks and \ comments, reaches VIOS unchanged. You rarely type it yourself:
the GUI Browser, the host CLI and the MCP tools (module_stage, develop)
do the encoding for you. The resident accepts up to 232 hex digits (116
bytes) per MODULE-DATA line, but a console line is limited to 235
characters, so 110 bytes (220 digits) per line is the safe choice. To produce MODULE-DATA lines on the Mac by hand:
python3 -c 'import sys;h=open(sys.argv[1],"rb").read().hex().upper();[print("MODULE-DATA",h[i:i+220]) for i in range(0,len(h),220)]' math.fth
Here is a complete example. The source of module MATH version 1 is:
: SQUARE ( n -- n*n ) DUP * ;
: CUBE ( n -- n^3 ) DUP SQUARE * ;
: MATH-OK 3 CUBE 27 = ;
MATH-OK is its test word. Begin, send the source and stage it:
MODULE-BEGIN MATH 1 MATH-OK
MODULE-DATA 3A205351554152452028206E202D2D206E2A6E202920445550202A203B0A3A20435542452028206E202D2D206E5E3320292044555020535155415245202A203B0A3A204D4154482D4F4B20332043554245203237203D203B0A
MODULE-STAGE
staged MATH crc32 4156190836
MODULE-STAGE compiled the source, ran MATH-OK, and then
undid everything: the module is now pending, and CUBE does not exist yet
(typing 5 CUBE . now gives "unknown word: CUBE"). Staging is your safe check
that the source compiles and passes its test. Only one module can be pending
at a time; activate or discard it before you stage another.
Activate it to make the words live:
MODULE-ACTIVATE MATH
5 CUBE .
MODULES
125 module MATH version 1 crc32 4156190836 bytes 89 deps 0 active 1 previous 0
MODULE-ACTIVATE compiles the source again in a new
transaction (the test word runs again) and only then updates the module
records. If that fails, nothing changes. MODULES lists every
active module; CRCs are printed in decimal (4156190836 is hex F7BA7074).
Dependencies
A module that uses words from another module names it, pinned by the source
CRC-32 of the exact generation it was written against. GEOM uses CUBE
from MATH version 1 (CRC F7BA7074):
: AREA ( w h -- a ) * ;
: BOX ( n -- vol ) CUBE ;
MODULE-BEGIN GEOM 1 -
MODULE-DEP MATH F7BA7074
MODULE-DATA 3A2041524541202820772068202D2D20612029202A203B0A3A20424F582028206E202D2D20766F6C20292043554245203B0A
MODULE-STAGE
MODULE-ACTIVATE GEOM
4 BOX .
Staging and activation fail with "missing or stale module dependency" if
MATH is not live or its current source has a different CRC. Dependencies
also decide the order in which modules are saved and loaded at boot.
MODULES shows each pin as a dependency GEOM module MATH crc32 ... line.
Generations, rollback and discard
Each module keeps two generations: the current one and the previous
one. Activating a new version moves current to previous. Version 2 of MATH
computes CUBE without calling SQUARE:
MODULE-BEGIN MATH 2 MATH-OK
MODULE-DATA 3A205351554152452028206E202D2D206E2A6E202920445550202A203B0A3A20435542452028206E202D2D206E5E3320292044555020445550202A202A203B0A3A204D4154482D4F4B20332043554245203237203D203B0A
MODULE-STAGE
MODULE-ACTIVATE MATH
MODULES
staged MATH crc32 358789367
module MATH version 2 crc32 358789367 bytes 88 deps 0 active 1 previous 1
module GEOM version 1 crc32 3299743760 bytes 50 deps 1 active 1 previous 0
dependency GEOM module MATH crc32 4156190836
A few things to know about generations:
- Redefinition, not replacement. Version 2 defines new words with the
same names. Code compiled earlier (here
BOXinGEOM) keeps calling the words it was compiled against, as usual in Forth.GEOMis now pinned to a generation ofMATHthat is no longer current; see the boot slot section for what that means. - Rollback covers the latest activation only.
MODULE-ROLLBACKMATHremoves everything the last activation compiled and restores the module records as they were. It works only for the most recent activation in the whole system and only while no module is pending. - A third accept retires the oldest generation. When you activate a third version, the oldest one is retired: rollback still undoes the latest activation, but the retired generation is gone from the module records. Its compiled code and source stay allocated (unnamed) until the next restart, so each accept uses some dictionary and source pool space. When a pool is full, staging refuses cleanly ("module source pool full"); save your work and restart to reclaim the space. The GUI Browser shows word, code and source-map use after each accept and asks you to save and restart above 80 %.
MODULE-DISCARDdrops a pending module (and any half-built one in the builder) without touching live code.MODULE-SOURCEMATHprints the current source assource-hexlines;MODULE-SOURCE MATH PREVIOUSprints the previous one.
Roll version 2 back, so that GEOM's pin is current again:
MODULE-ROLLBACK MATH
MODULES
module MATH version 1 crc32 4156190836 bytes 89 deps 0 active 1 previous 0
module GEOM version 1 crc32 3299743760 bytes 50 deps 1 active 1 previous 0
dependency GEOM module MATH crc32 4156190836
Typical errors
| Message | Cause |
|---|---|
module test must return one true flag |
the test word left 0, nothing, or more than one value |
I/O disabled during module transaction |
the source prints or reads at load time |
native I/O disabled during module transaction |
the source calls a device service at load time |
module initialization leaves stack values |
the source left numbers on the stack |
incomplete module definition |
the source ends inside a : definition |
discard or activate the pending module first |
another module is already staged |
no matching staged module |
MODULE-ACTIVATE with a name that is not pending |
missing or stale module dependency |
a pinned module is not live or has another CRC |
only the latest activation has a rollback checkpoint |
rollback of an older activation |
module inventory full (8 modules) |
the ninth module |
module source pool full (32768 bytes) |
retired generations filled the pool: save and restart |
Packages: SAVE and MODULE-LOAD
SAVE writes a module's current generation to the SD card as a
package (.VIM file): a 260-byte header (name, version, test word, the
pinned dependencies, length and CRC of the source), the exact source, and a
CRC-32 over the whole file.
SAVE GEOM
saved /VIOS/2E5A9E54/2BE19214.VIM
The default path is content-addressed: the directory is the CRC-32 of the
module name, the file name is the CRC-32 of the package. A package file is
never overwritten: saving the same package again is harmless, and different
data at an existing path is refused as a collision. Files are written as
.TMP, read back, compared and only then renamed, so an interrupted save
cannot leave a damaged package under its final name.
SAVE first saves every module the module depends on, recursively, using the
pinned generation (the previous one if the dependency has moved on since; if
neither generation matches, it fails with "dependency source no longer
retained"). With a second argument it also writes a copy under a name of your
choice, which needs an 8.3 name with an extension:
SAVE MATH /VIOS/MATH.VIM
MODULE-LOAD reads a package, checks the header and both
CRCs, and compiles and activates it in one transaction (its dependencies must
already be live):
MODULE-LOAD /VIOS/MATH.VIM
Saving a package does not make it load at boot. Boot loads the modules listed in the confirmed boot slot, described next.
Boot slots A and B
VIOS keeps two boot slots on the SD card, /VIOS/BOOT_A.VIB and
/VIOS/BOOT_B.VIB. Each is a small bundle: a list of module names, the
paths of their packages and their source CRCs, in dependency order, protected
by a CRC-32. A boot record (journaled in two files, /VIOS/BOOT0.REC and
/VIOS/BOOT1.REC, the newer valid one wins) says which slot is
confirmed, which one was the previous confirmed slot, and whether a
candidate is waiting to be tried.
The commands:
| Command | Effect |
|---|---|
BOOT-TRIAL |
save every active module and write them into the slot that is not confirmed; mark it as the candidate |
BOOT-STATUS |
show confirmed, candidate, state, the slot loaded at this boot, record generation |
BOOT-MANIFEST |
read-only: what a power cycle would load, module by module, with package health |
BOOT-CONFIRM |
confirm the running trial (normally already done by startup) |
BOOT-REVERT |
cancel a pending trial, or swap back to the previous confirmed slot |
BOOT-TIMES |
how long each startup step took, including the boot-module cache |
BOOT-TRIAL takes all active modules, not just one. It fails with "boot
bundle has missing/stale dependency" if a module is pinned to a generation
that is no longer current, as GEOM was after MATH version 2. Fix that by
staging the dependent module again with the new CRC, or by rolling back.
BOOT-TRIAL
BOOT-STATUS
BOOT-MANIFEST
boot confirmed NONE candidate A state PENDING current NONE generation 1
manifest NONE confirmed none
manifest A candidate modules 2 crc32 1517667278
manifest-module A MATH version 1 crc32 4156190836 package ok
manifest-module A GEOM version 1 crc32 3299743760 package ok
What happens at the next boot (after REBOOT or a power cycle):
- If a candidate is pending, VIOS marks it as tried, loads its modules and
runs a health check (a Forth self-test, a display query to the ESP and an
SD sync). If everything passes, the candidate becomes confirmed
automatically and the console shows
boot trial confirmed A. - If the candidate fails, the console shows
boot candidate rejected:and the reason, and VIOS boots the confirmed slot instead. - If the confirmed slot fails too, it tries the previous confirmed slot (and makes it the confirmed one if it works).
- If no slot works, VIOS starts the built-in kernel with no modules:
boot recovery: built-in kernel, no valid source bundle.
Because a healthy trial confirms itself, you normally never type
BOOT-CONFIRM; it succeeds without changes when the running slot is already
confirmed. BOOT-REVERT never reboots: it cancels a pending candidate, or, if
none is pending, makes the previous confirmed slot the confirmed one again for
the next boot.
Both slots live on the same FAT32 card: the A/B scheme protects you from a bad update, not from a damaged card.
The boot-module cache
Compiling every module at each boot takes time (about 2.2 s for a typical
set). After a slot's modules are loaded, VIOS saves a snapshot of what the
loads added (dictionary, compiled code, data space and module records) to
/VIOS/CACHE_A.VIC or /VIOS/CACHE_B.VIC. The next boot of the same slot
restores the snapshot instead of compiling (measured: 857 ms instead of
2,168 ms).
The cache never decides what runs:
- It is keyed by the resident build stamp (a CRC of the firmware binary), the
slot bundle's CRC and the state of the base dictionary. A new firmware, a
new
BOOT-TRIALor any other difference makes it stale. - The bundle is still read and checked first, and after a restore every module's identity is checked against the bundle.
- On any mismatch or damage VIOS compiles the packages as usual and rewrites the cache. Unstamped builds (host builds, ELF loads) never use it.
BOOT-TIMES reports the result as cache hit, stale, corrupt, absent,
unstamped, identity or saved, with hit, miss and save counts:
BOOT-TIMES
boot-times storage 41 state 12 modules 857 (0 loads, slowest - 0; cache hit hits 1 misses 0 saves 0; read 0 check 0 compile 0) health 325 finish 3 total 1238 ms
(An illustration of a cached boot: no package was compiled, so loads is 0.
Your board prints its own numbers; the host build prints zeros.)
What is and is not persisted
| Kept across a restart | Not kept |
|---|---|
| Modules in the confirmed boot slot (current generation) | Words typed at the console outside a module |
Package files written by SAVE or BOOT-TRIAL |
The pending (staged) module and the rollback checkpoint |
| Boot records and slot bundles | Previous and retired generations |
/VIOS/USER.FTH and any other file you write |
Variable values changed after boot (module initialisation runs again) |
| Tasks, handles, pipes and devices | |
| Tunes stored on the ESP, windows and other GUI state |
For settings that are not code, such as a preferred BASE or a startup
message, put Forth lines in /VIOS/USER.FTH. VIOS loads it once at each boot
after the boot slot (see Files and the SD Card).
A word named AUTORUN in a boot module is started as a task after that (see
Tasks and Preemption).
The everyday workflow
The cycle is always the same: edit, stage, activate, save, trial, confirm.
- Edit the source: in the GUI Browser's code pane, in a file on the Mac, or in your head.
- Stage it: compile and run its test in a dry run. Fix and repeat until it passes.
- Activate it: the words go live. Try them at the console. If version 2
misbehaves,
MODULE-ROLLBACKit. - Save it:
SAVE namewrites the package (optional,BOOT-TRIALsaves too, but it is a cheap checkpoint). - Trial it:
BOOT-TRIALwrites all active modules into the inactive slot. - Restart and check:
REBOOT(or power-cycle), thenBOOT-STATUS. The console saidboot trial confirmedandcurrentis the new slot. If the trial was rejected, the old slot booted and nothing is lost.
From the console the whole cycle looks like this (source lines shortened):
MODULE-BEGIN MATH 3 MATH-OK
MODULE-DATA 3A2053515541...
MODULE-STAGE
MODULE-ACTIVATE MATH
SAVE MATH
BOOT-TRIAL
REBOOT
From the Mac, the MCP tool develop does stage, activate and (optionally)
save in one call and stops at the first failure; boot_trial, boot_status
and boot_manifest do the rest (see
The Host Harness and MCP Tools).
In the GUI, open the System Browser with Ctrl+Shift+B, pick a module, and
edit its source in the code pane. Ctrl+S (Accept) stages and activates the
text in one step; errors appear in the status line. The pane menu
(Shift+F10) has Save to SD, Revert and Rollback. A word outside any
module is simply re-evaluated when you accept it. The Browser is described in
The GUI Tools; BOOT-TRIAL and the restart are the same as
from the console.