Modules and Persistence

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 console BASE is.
  • It may define words, variables, constants and deferred words, and run initialisation code, but it may not do I/O: EMIT, ., CR, KEY and every device service fail with "I/O disabled during module transaction". SPAWN and 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 BOX in GEOM) keeps calling the words it was compiled against, as usual in Forth. GEOM is now pinned to a generation of MATH that is no longer current; see the boot slot section for what that means.
  • Rollback covers the latest activation only. MODULE-ROLLBACK MATH removes 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-DISCARD drops a pending module (and any half-built one in the builder) without touching live code.
  • MODULE-SOURCE MATH prints the current source as source-hex lines; MODULE-SOURCE MATH PREVIOUS prints 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):

  1. 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.
  2. If the candidate fails, the console shows boot candidate rejected: and the reason, and VIOS boots the confirmed slot instead.
  3. If the confirmed slot fails too, it tries the previous confirmed slot (and makes it the confirmed one if it works).
  4. 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-TRIAL or 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.

  1. Edit the source: in the GUI Browser's code pane, in a file on the Mac, or in your head.
  2. Stage it: compile and run its test in a dry run. Fix and repeat until it passes.
  3. Activate it: the words go live. Try them at the console. If version 2 misbehaves, MODULE-ROLLBACK it.
  4. Save it: SAVE name writes the package (optional, BOOT-TRIAL saves too, but it is a cheap checkpoint).
  5. Trial it: BOOT-TRIAL writes all active modules into the inactive slot.
  6. Restart and check: REBOOT (or power-cycle), then BOOT-STATUS. The console said boot trial confirmed and current is 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.