Skip to content

Undo/Redo and Advanced Cases

Time travel: :undo / :redo / :history

Every successful swap saves a checkpoint in a per-module ring buffer (default: 10 entries). At the zolo dev prompt you can navigate this history without manually rewriting code.

Available prompt commands:

Command Effect
:undo Reverts the most recent swap
:redo Re-applies the last :undo
:history Lists checkpoints and counters
:reload <module> Forces the swap of a module
:eval <expr> Compiles and runs an expression in the live VM
:stats p50/p95 latencies and swap count

Live state is preserved in both directions: a pub let count = 5 remains 5 after :undo, even if the old version had the literal 0. The redo stack is cleared when a fresh swap occurs after a :undo.

Edit the formula several times; use :undo/:redo to navigate between versions.

formula.zolo
// Feature: a perfect target to experiment with :undo / :redo
// Syntax: edit the formula multiple times; navigate the history.
// When to use: compare variants of a function without rewriting
// each one by hand. The `ReloadHistory` keeps the source of
// previous versions and re-applies them via swap.

pub fn calc(x: int) -> int {
  // Edit this formula several times during the `dev` session.
  // Each save adds a checkpoint; :undo rolls back one step.
  return x * 2
}

Requires the Zolo CLI/host — open in the playground or run locally.

main.zolo
// Feature: time-travel of hot-reloads — :undo / :redo / :history
// Syntax: commands at the `zolo dev` prompt (not Zolo words).
// When to use: experiment with variants of a function and roll
// back to a previous one quickly without editing by hand.

use formula::{calc}

fn main() {
  print("ENTER prints calc(10). Use :undo / :redo / :history at the prompt.")
  while true {
    let line = io::read("*l")
    if line is nil || line == "q" {
      break
    }
    print("calc(10) = {calc(10)}")
  }
}

Requires the Zolo CLI/host — open in the playground or run locally.

Reload in a tight loop without yield

Code that never yields control (no io::read, no sleep) still receives swaps: the VM's instruction hook drains pending reloads every ~2000 instructions. The worker.zolo module can be edited while the counter spins — the next call to tick already uses the new version.

Edit the message in tick() while the loop runs — no manual action required.

worker.zolo
// Edit this while the demo is running. Save with Ctrl+S — the next
// `tick` call uses the new code with no manual intervention.

pub fn tick(i) {
  print("tick {i}: still on V1 — edit me!")
}

Requires the Zolo CLI/host — open in the playground or run locally.

main.zolo
// Demo 02 — Hot-reload during a TIGHT CPU LOOP.
//
//   zolo dev examples/zolo_live/02_tight_loop/main.zolo
//
// This script has NO `io::read` or any other yield point. It just busies
// itself spinning a counter. Yet edits to `worker.zolo` take effect
// within milliseconds — the VM instruction hook drains pending swaps
// every ~2000 instructions, transparently to the user.
//
// While running, edit `worker.zolo` (e.g. change the message format)
// and watch the output update LIVE without any user action.

use worker::{tick}

fn main() {
  let i = 0
  let last_print = 0
  while true {
    i = i + 1
    if i - last_print > 5_000_000 {
      tick(i)
      last_print = i
    }
  }
}

Requires the Zolo CLI/host — open in the playground or run locally.

Closures captured outside the live-binding

When a function reference is copied directly into a list or data structure (bypassing the thunk), the runtime still updates that reference. During the swap, the collector walks the heap and replaces every pointer to the old function with the new one. The 09-closure-escape demo illustrates this with a fn_queue that accumulates references to greeter.greet before any edits:

Directly captured references are also updated on swap.

greeter.zolo
// Edit me — the queued function references in main.zolo update too,
// thanks to closure-correct hot-reload.

pub fn greet(who) {
  print("V1: hi {who}")
}

Requires the Zolo CLI/host — open in the playground or run locally.

main.zolo
// Demo 04 — Closure-correct hot-reload.
//
//   zolo dev examples/zolo_live/04_closure_escape/main.zolo
//
// The script captures a function reference into a queue (`fn_queue`),
// then iterates and calls them. Even though the function value escapes
// the live-binding thunk, edits to greeter.zolo still take effect on
// next call: the closure registry walks the heap and patches every
// reference to the old function value with the new one.
//
// This is the deeper guarantee — works no matter how the function got
// captured.

mod greeter

fn main() {
  let fn_queue = []
  // Capture the function value DIRECTLY (no live-binding thunk):
  fn_queue.push(greeter.greet)
  fn_queue.push(greeter.greet)
  fn_queue.push(greeter.greet)

  print("Press ENTER to flush the queue — or 'q' to quit.")
  print("Edit greeter.zolo while waiting; queued fn refs update too.")
  while true {
    let line = io::read("*l")
    if line is nil || line == "q" {
      break
    }
    for f in fn_queue {
      f("queued")
    }
  }
}

Requires the Zolo CLI/host — open in the playground or run locally.

enespt-br