This is the thesis. The dictionary is append-only and lookup runs backwards, so redefining a word shadows the old one rather than mutating it. That means the engine's entire state is three fill pointers — and rolling back is just truncating them.
forth_eval_rollback() uses that to apply remotely-delivered code safely: it takes a savepoint, evaluates line by line, and if any line fails it reverts everything and tells you which line broke.
Applying a bundle from C
const char *bundle =
": helper 2 * ;\n"
": role-tick helper . ;\n";
char failing[128];
int rc = forth_eval_rollback(bundle, strlen(bundle), failing, sizeof failing);
if (rc == 0) {
/* every line took effect — safe to persist the bundle verbatim */
} else if (rc == 1) {
ESP_LOGW(TAG, "bundle rejected at: %s", failing);
/* dictionary is already back where it was */
}
Here is an actual run. A good bundle redefines role-tick; a bad one is rejected whole, leaving the previous definition intact:
// starting point
ok> : role-tick ." v1" ;
ok> role-tick
v1
// apply a GOOD bundle: helper + a new role-tick
apply GOOD bundle -> rc=0
ok> 21 role-tick
42
forth_word_exists("helper") = 1
// apply a BAD bundle: a valid line, then a broken one
? compile: nonexistent-word
apply BAD bundle -> rc=1
failing line = ": role-tick nonexistent-word ;"
forth_word_exists("broken") = 0 <-- the good line was rolled back too
ok> 21 role-tick
42 <-- still the previous version
Note that broken — defined by the bundle's first line, which succeeded — is gone. The rollback is all-or-nothing across the bundle, which is what makes it safe to send code to a device you cannot physically reach.
Know what the savepoint does not cover. It restores the dictionary, the code area and the heap pointer. It does not restore the contents of variables that existed before the savepoint, the data stack, or the number base. A bundle whose last successful act was 99 config ! leaves that store in place after a rollback.