Skip to content
Tooling

velt dev: hot reload#

Text
velt dev [<file.vlt>] [--exe [--release] [-g] [--cpu <name>] [--backend <name>]] [--locked] [-v] [--timings] [-- <program args>...]

velt dev runs your program like velt run, then keeps it up to date while you edit. Save a file and the running program picks up the change, usually with its in-memory state and open connections intact.

Text
$ velt dev server.vlt
velt dev: started in 410 ms
velt dev: hot-swapped 3 functions in 82 ms
velt dev: build failed (the previous version keeps running); waiting for changes
velt dev: restarted (Point gained a field) in 380 ms

What happens on save#

velt dev watches every file the build read (your modules, the standard library, path dependencies) plus package.vlt and velt.lock.json, and new .vlt files next to them (so a module that an import was missing is picked up as soon as you create it). Changes come from the operating system's file notifications; where those don't work (some network or container file systems), set VELT_DEV_POLL=1 to check the files every 10 ms instead. A file saved while a build is running leads to another build once it finishes. Files are compared by modification time and length, so on file systems with coarse timestamps (FAT, some network mounts) two saves of the same length within one timestamp tick can look like one. On a change it builds the new version while the old one keeps running:

  • Hot swap (the common case): the changed functions are compiled and swapped into the running program. In-memory data, open connections, caches and running tasks survive. New calls and requests run the new code; work already in flight finishes on the old code.

Restart#

When live state could not survive the edit, a new version starts instead, and the reason is printed. That happens when a struct or class layout changed, a closure's captures changed, a function's signature changed, main changed (it has already run), a function that live values may still call was removed, or closures were reordered.

  • Build error: the diagnostics are printed and the old version keeps running.
  • Program exit: velt dev prints the exit code and waits for the next change.

Crash during a reload#

If the running program dies while it takes a change (a bug in velt dev, please report it), velt dev says so, for example velt dev: the running program crashed during the reload (exit code 0xc0000005); restarted it in 950 ms, and the new version runs from a fresh start.

Stopping velt dev (Ctrl-C, or SIGTERM/SIGHUP from a process manager or docker stop) stops the program the same way a reload does: it gets a stop request and can finish in-flight requests; a program still running 1.5 s after the request is killed (set VELT_DEV_STOP_GRACE_MS to change that limit, in milliseconds). velt dev waits for it before exiting. Press Ctrl-C twice to exit at once.

Listening sockets survive restarts: velt dev owns them and hands them to each version, so no connection is refused during a reload and a server on port 0 keeps its port. State in values that main creates carries over a hot swap. Module state (module-level lets) does not: the swapped-in code starts it over from its initializers. A const X = new Atomic<T>(literal) is not module state (sync): it keeps its value across swaps, also when the edit changes its starting literal (the first version's value stays). An edit that changes its type gives it a new word, which starts at the new literal.

Modes#

  • JIT (the default): each version is compiled in memory with Cranelift and runs inside a velt host process. There is no link step and no new executable, which also avoids the operating system's first-launch checks of new binaries on macOS and Windows.

--exe#

Each version is a linked debug executable (target/velt/dev/<name>-<n>), so you can attach a debugger to it (Debugging). Edits restart the program instead of hot-swapping. --release, -g, --cpu and --backend are accepted with --exe only (the JIT compiles for this machine's CPU).

velt test --watch reruns the tests on every change the same way (Testing).

Speed#

Save to first response with a new handler body, measured by velt's reload benchmark (bench/reload/RESULTS.md; Windows 11 x86_64, i9-12900HK, release velt, other builds running on the machine):

ModeMedian
JIT, hot swap64 ms
JIT, restart378 ms
--exe, restart2.6 s

The time includes a 30 ms settle delay after the last write.

-v prints how long each build stage took; --timings also splits the JIT stage into compiling, finalizing, registering unwind information and building the debug-info image.

Limits#

  • Code that is already running keeps running the old version: a future in flight (by design), the rest of main's body, and an endless loop inside one async function. The functions they call do swap.
  • Editing an async function that main awaits directly restarts the program. The call is already running (a server loop, say), and running code keeps its version, so a swap would have no visible effect; a restart runs the new code. To keep state across such edits, move the work into functions the long-running one calls (a request handler, a loop body): those swap.
  • Edits to functions that only ran during startup are swapped, but they don't run again.
  • After 200 hot swaps the host restarts to reclaim the memory of old code.
  • JIT code has line-level debug information for GDB and LLDB on Linux; macOS is untested (LLDB needs plugin.jit-loader.gdb.enable on) (Debugging). VELT_DEV_DEBUG_INFO=0 turns it off. On Windows, use --exe or a normal build to debug.
  • JIT code registers its unwind information on Windows x64, macOS and Linux, so debuggers and backtraces walk through it; on Windows arm64 it does not yet. On musl (Alpine) the JIT host is unavailable, so use --exe. Hot swap is tested end to end on Windows x64 and Linux x86_64; macOS builds the same code, but the reload tests have not been run there yet.

Edit this page on GitHub